xiyuan技术圈

溪源的技术博客,技术的不断进取之路


  • Home

  • 技术

  • 随笔

  • 读书

  • 管理

  • 归档

提高编程技能的5种方法

Posted on 2021-04-25 | In 技术

提高编程技能的5种方法

David Bolton https://insights.dice.com/2017/08/29/5-ways-improve-programming-skills/

即使拥有40年的编程经验,我唯一能确定的就是肯定有比我更好的程序员。但是我并没有放弃,我会继续尝试并提高自己的编程技能。

我认为有五件事可以帮助任何人成为更好的程序员。有些是我个人的经验,有的则是我从网上和其他程序员那里学到的方法。

查找适合您的学习方法

我们每个人都以不同的方式学习。例如,我不是视频学习的忠实拥护者,而是宁愿从书本或网站上学习。而且我敢肯定我并不孤单:对于许多人来说,一段30分钟的视频(甚至更长)太慢了。这就是为什么YouTube上一些最受欢迎的编程视频往往很短的原因-在许多情况下,时间在5到12分钟之间。

如果您在学习视频方面遇到困难,建议您去上学,阅读。回到我刚开始的时候,编译器附带了广泛的纸质手册,您可以从头到尾阅读。现在所有软件文档似乎都在线上,您可以按照自己的进度阅读。

如果您不是被动学习的爱好者(无论是基于视频还是基于文本),那么还有另一种可行的方法:编写比赛代码。您不必取胜,但是将自己的技能与其他人置于快节奏的环境中通常会教给您一些新的技巧。而且,如果您确实赢了-嘿,通常会有现金奖。Project Euler,TopCoder,Coderbyte等几个网站都可以挑战您。

无论选择哪种教育方式,都要确保离开舒适区。您需要投入未知才能成为更好的程序员。即使您有多年的经验,仍然有很多东西要学习。

简化您的代码

只要有问题的代码写得好并且易于遵循,阅读其他人生成的代码就很有见识。如果您在编写和维护代码时遇到麻烦,检查有经验的程序员如何处理类似的任务是挑选一些指针的好方法。通常,您会发现这些专业人士尽可能地简化了他们的代码。

这是我多年来积累的一些简化技巧:

删除明显的复制和粘贴代码。如果您在不同的地方看到相同的代码,则很可能成为类中的函数或方法的候选者。

将大型代码块拆分为较小的代码块。同样,您可以将这些巨大的块简化为函数或方法,即使它们仅被调用一次:具有有意义名称的代码块也更易于遵循。您并非总是能够将代码简化为单一功能,但这是您可以追求的目标。

使用重构工具使变量名更具描述性。另外,添加有意义的注释。从简单的函数开始,以便您可以理解它们,并力求在每个函数,类和方法的顶部至少有一行注释。

学习不同的编程语言

同样,作为一名程序员而提高意味着要超越自己的舒适区。例如,如果您了解面向对象的语言,请尝试一种功能性的语言。对于C#,显而易见的功能语言是F#。同样,如果您仅编程桌面应用程序,请尝试学习移动或Web开发;如果您知道C#,则可以将Xamarin用于移动开发,也可以将ASP.NET MVC用于Web。我知道台式机C#和Xamarin,但不了解ASP.NET MVC,我最近开始学习在线课程。

尽管它可能对您的编程无济于事,但我鼓励Windows开发人员学习如何使用Linux。您可以将其安装在旧PC或Windows机器上;如果要对网站进行编程,请尝试在Linux上设置Apache Web服务器。

学习新编程语言的首选方法是转换现有的实用程序或小型程序。因为您已经知道它的作用以及它的工作方式,并且获得了输出或测试结果,所以使用新语言确实可以简化学习过程。例如,我通过获取现有Z80编辑器/汇编器的源代码并从中编写6502交叉汇编器来学习Z80汇编器编程。我没有调试器,这可能有点极端,但是它对我有用。

探索不同的做事方式

这可能是这五件事中最难做到的。遇到编程问题时,我的直觉总是锁定解决问题的特定方式。但是通常有更多的方法可以解决它。一旦用通常的方法解决了问题,就可以通过更快或更节省内存的方法来重做舒适区。起初可能会令人沮丧,但它可能使您成为功能更丰富的程序员。

在调试器中单步执行代码

您可能会认为所有程序员都这样做;但是在我的职业生涯中,我遇到了至少两个没有找到的人,他们让一个漏洞被客户发现。(“生产测试”,我曾听说过这种说法,但这并不是对职业的友好举动。)如果您确信自己的软件没有错误,那么可以采用著名计算机科学家Donald Knuth的方法:他将奖励支票邮寄给发现自己编写的软件有错误的任何人。

不愿意冒险吗?单步执行代码。您将对正在发生的事情有一个更好的了解,确定自己思维中的薄弱环节,并在下次构建类似的软件时学习做什么(或不做什么)。

结论

我会练习这些技巧吗?多半是对的。正如任何程序员所知道的那样,当您忙于工作时,很难找到学习和成长的时间。尽管如此,重要的是要保留时间尝试新事物。尽可能地保持技能的磨练不仅是保留您的工作,而且还可以完成您实际想要做的项目的最佳方法。

如何在我们的Asp.NET Core应用程序中使用ElasticSearch高级功能

Posted on 2021-04-25 | In 技术

在上一篇文章中,我们讨论了将ElasticSearch用作简单的全文本搜索引擎,如何快速安装和配置它以及如何将其集成到我们的.NET Web应用程序中。

今天,我们仍然要在电子商务网站中向您展示如何使用ElasticSearch的许多功能来改善搜索。

我们使用了没有嵌套类的平面Product类来轻松管理搜索,但是这种方法有很多限制。然后,我们引入了一个新的数据模型,以便任何对象都是要建模的实体。一个文档可以包含无限数量的相关字段和值(数组,简单和复杂类型),并保存为JSON文档。

我们的模型产品类别已变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Product
{
    public int Id { get; set; }
    public string Ean { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Brand Brand { get; set; }
    public Category Category { get; set; }
    public Store Store { get; set; }
    public decimal Price { get; set; }
    public string Currency { get; set; }
    public int Quantity { get; set; }
    public float Rating { get; set; }
    public DateTime ReleaseDate { get; set; }
    public string Image { get; set; }
    public List<Review> Reviews { get; set; }
}

其中品牌,类别,商店评论和用户类别分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Brand
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}
 
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}
 
public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}
 
public class Review
{
    public int Id { get; set; }
    public short Rating { get; set; }
    public string Description { get; set; }
    public User User { get; set; }
}
 
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string IPAddress { get; set; }
    public GeoIp GeoIp { get; set; }
}

GeoIp是NEST库中用于地理数据的类。
产品索引已被简单地命名为产品。我们以这种方式创建和配置了它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
client.Indices.Create(“products”, index => index
    .Map<Product>(x => x.AutoMap())
    .Map<Brand>(x => x.AutoMap())
    .Map<Category>(x => x.AutoMap())
    .Map<Store>(x => x.AutoMap())
    .Map<Review>(x => x.AutoMap())
    .Map<User>(x => x.AutoMap()
        .Properties(props => props
            .Keyword(t => t.Name("fullname"))
            .Ip(t => t.Name(dv => dv.IPAddress))
            .Object<GeoIp>(t => t.Name(dv => dv.GeoIp))
        )
    )
)

我们专门为ElasticSearch索引创建一个名为fullname的新属性,用于名为fullname的User类,并定义了将要处理的地理信息。
为了使我们的产品能够在索引之前进行处理,一种有用的方法是摄取节点,即进行文档预处理的节点。接收节点拦截所有索引请求,甚至是批量索引请求,并将所有定义的转换应用于其内容,然后将文档发还给索引API。

必须通过以下参数在配置文件elasticsearch.yml中启用摄取:

node.ingest: true

在我们的示例中,我们使用相同的节点进行搜索和摄取,我们不需要编写代码来管理摄取节点,但是,如果我们要拥有一组专用的摄取节点,则必须配置ElasticSearch客户如下:

1
2
3
4
5
6
7
8
var pool = new StaticConnectionPool(new [] 
{
    new Uri("http://ingestnode1:9200"),
    new Uri("http://ingestnode2:9200"),
    new Uri("http://ingestnode3:9200")
});
var settings = new ConnectionSettings(pool);
var client = new ElasticClient(settings);

为了对文档进行预处理,需要在建立索引之前定义一个管道,该管道指定一组能够转换该文档的过程。有许多默认过程可供使用。例如:GeoIP从IP地址获取地理信息,JSON将字符串转换为JSON对象,小写和大写,Drop删除与某些参数匹配的文档。您也可以创建自定义过程。
我们在项目中使用的管道是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
client.Ingest.PutPipeline("product-pipeline", p => p
                .Processors(ps => ps
                    .Uppercase<Brand>(s => s
                        .Field(t => t.Name)
                    )
                    .Uppercase<Category>(s => s
                        .Field(t => t.Name)
                    )
                    .Set<User>(s => s.Field("fullname")
                        .Value(s.Field(f => f.FirstName) + " " +
                             s.Field(f => f.LastName)))
                    .GeoIp<User>(s => s
                        .Field(i => i.IPAddress)
                        .TargetField(i => i.GeoIp)
                    )
                )
            );

该管道处理文档,以便:

  • Brand.Name和Category.Name将通过大写输入以大写形式索引;
  • User.fullname将包含名字和姓氏(设置摄取);
  • User.IPAddress将成为地理定位的地理地址(GeoIp提取)。

管道以ElasticSearch集群状态保存,要使用它们,您必须在索引请求中指定管道参数,以便摄取节点知道必须使用哪个管道:

1
2
3
4
5
6
7
8
9
10
11
client.Bulk(b => b
    .Index("products")
    .Pipeline("product-pipeline")
    .Timeout("5m")
    .Index<Person>(/*snip*/)
    .Index<Person>(/*snip*/)
    .Index<Person>(/*snip*/)
    .RequestConfiguration(rc => rc
        .RequestTimeout(TimeSpan.FromMinutes(5))
    )
);

通过这种方式,我们定义了索引编制过程,以便我们可以根据需要获取文档清单。
在使用创建的管道为文档建立索引之后,我们可以通过使用浏览器http:// localhost:9200 / products / _search进行访问来检查它们。我们得到类似于以下结果:

图片

如上一篇文章所述,搜索过程基于文档分析。这是第一个阶段的令牌化过程(将文本分成小块,称为令牌),另一个是规范化过程(它允许您查找与不等于搜索词但足够相似以至于相关的令牌的匹配项)为搜索建立索引的文本。分析仪执行此过程。

分析仪由三个主要部分组成:

  1. 0个或多个字符过滤器
  2. 1个分词器
  3. 0个或多个令牌过滤器

图片有一些默认的分析器可以使用,但是,为了根据我们的要求提高搜索的准确性,我们创建了一个自定义分析器。

定制分析器使我们能够在分析过程中控制令牌化之前对文档的任何更改,如何将其转换为令牌以及如何对其进行规范化。

这是我们的自定义分析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var an = new CustomAnalyzer();
an.CharFilter = new List<string>();
an.CharFilter.Add("html_strip");
an.Tokenizer = "edgeNGram";
an.Filter = new List<string>();
an.Filter.Add("standard");
an.Filter.Add("lowercase");
an.Filter.Add("stop");
 
settings.Analysis.Tokenizers.Add("edgeNGram", new Nest.EdgeNGramTokenizer
{
    MaxGram = 15,
    MinGram = 3
});
 
settings.Analysis.Analyzers.Add("product-analyzer", an);

我们的分析器使用标准的标记化方法,创建3至15个字符的小写标记。我们可以将分析器添加到一个或多个字段的索引中,也可以将其添加为标准分析器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
client.CreateIndex("products", c => c
    // Analyzer added only for the property Description of Product
    .AddMapping<Product>(e => e
        .MapFromAttributes()
        .Properties(p => p.String(s => s.Name(f => f.Description)
        .Analyzer("product-analyzer")))
    )
    //Analyzer added as default
        .Analysis(analysis => analysis
            .Analyzers(a => a
            .Add("default", an)
        )
    )
)

创建自定义分析器时,可以使用测试API对其进行测试。即使对于默认分析仪,也可以执行这些测试。

1
2
3
4
5
var analyzeResponse = client.Indices.Analyze(a => a
    .Tokenizer("standard")
    .Filter("lowercase", "stop")
    .Text("Lorem ipsum dolor sit amet, consectetur...")
);

我们还可以使用数据聚合来提供通过搜索查询聚合的数据,它基于可以组成以获得复杂聚合的简单块。聚合有不同类型,每种类型都有定义的范围和输出。
它们可以分为:

  • 桶装:具有关键和标准的容器;
  • 指标:根据一组文档计算的指标;
  • 矩阵:在不同文档字段上进行的一系列操作,以矩阵样式生成数据;
  • 管道:更多聚合的聚合。

在我们的案例中,我们使用汇总来获取品牌,类别,价格范围的产品数量。在以下示例中,我们找到了产品价格的汇总:

1
2
3
4
5
6
7
s => s
    .Query(...)
    .Aggregations(aggs => aggs
        .Average("average_price", avg => avg.Field(p => p.Price))
        .Max("max_price", avg => avg.Field(p => p.Price))
        .Min("min_price", avg => avg.Field(p => p.Price))
    )

另一个有用的聚合是根据品牌,商店或类别进行分组:

1
2
3
4
5
6
7
s => s
     .Query(...)
     .Aggregations(aggs => aggs
         .ValueCount("products_for_category", avg => avg.Field(p => p.Category.Name))
         .ValueCount("products_for_brand", avg => avg.Field(p => p.Brand.Name))
         .ValueCount("products_for_store", avg => avg.Field(p => p.Store.Name))
     )

这样,我们可以实时获取针对类别,品牌和商店的搜索产品数量。汇总数据还可以用于创建仪表板,甚至可以使用动态过滤器(类似于电子商务)来组织搜索,并且显然可以用于统计目的。

改善您的搜索

如您所知,我们对任何搜索结果都有分数。等级是从0到1的数字,它确定搜索参数如何接近该结果。得分主要取决于三个参数:搜索词的频率,倒排文档的频率和字段长度。

要从得分中排除得分过低的人,我们可以使用MinScore:

1
2
3
s => s
     .MinScore(0.5)
     .Query(...)

这样,我们可以排除分数低于0.5的所有结果。
建议者允许您使用与搜索文本相似的术语来搜索ElasticSearch索引。例如,完成建议器对于自动完成很有用,它会在键入文本时引导您获得最佳和更相关的结果。该完成建议程序经过优化,可以尽快返回结果,但是它使用启用了快速查找的结构并需要资源。

在我们的案例中,我们实现了基于产品名称的自动完成方法,该方法将在搜索框中键入以下内容时被调用:

1
2
3
4
5
6
7
8
9
10
11
s => s
    .Query(...)
    .Suggest(su => su
        .Completion("name", cs => cs
            .Field(f => f.Name)
            .Fuzzy(f => f
                .Fuzziness(Fuzziness.Auto)
            )
            .Size(5)
        )
    )

更好的搜索的另一有用方法是索引boost。当您搜索更多索引时,可以为这些索引分配一个乘数,这样一来,一个索引的结果将比另一个显示更多。您可以将其用于商业目的,与供应商达成协议或使我们的产品脱颖而出。
索引提升的一个示例是:

1
2
3
4
5
6
s => s
    .Query(...)
    .IndicesBoost(b => b
        .Add("products-1", 1.5)
        .Add("products-2", 1)
    )

在此示例中,我们将乘数1.5乘以1的结果,乘以1乘以2的结果,这样乘积1的结果将被更频繁地显示。
改进搜索的另一种方法是通过一些参数对它们进行排序。就我们而言,我们可以:

1
2
3
4
5
6
7
8
s => s
    .Query()
    .Sort(ss => ss
        .Descending(SortSpecialField.Score)
        .Descending(p => p.Price)
        .Descending(p => p.ReleaseDate)
        .Ascending(SortSpecialField.DocumentIndexOrder)
    )

我们将评分,价格,发布日期以及最终索引顺序设置为更高的优先级。

运行项目

我们的示例项目是一个.NET Core MVC WebApi应用程序,该应用程序提供一个搜索框和一个仪表板,其中的仪表板会根据键入的文本自动刷新数据。首次运行项目时,我们可以加载由Bogus插件创建的n个Product对象。还有其他伪造类可以为品牌,类别,商店,评论和用户构建随机对象。它允许您拥有一个数据库来执行我们的搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var productFaker = new Faker<Product>()
    .CustomInstantiator(f => new Product())
        .RuleFor(p => p.Id, f => f.IndexFaker)
        .RuleFor(p => p.Ean, f => f.Commerce.Ean13())
        .RuleFor(p => p.Name, f => f.Commerce.ProductName())
        .RuleFor(p => p.Description, f => f.Lorem.Sentence(f.Random.Int(5, 20)))
        .RuleFor(p => p.Brand, f => f.PickRandom(brands))
        .RuleFor(p => p.Category, f => f.PickRandom(categories))
        .RuleFor(p => p.Store, f => f.PickRandom(stores))
        .RuleFor(p => p.Price, f => f.Finance.Amount(1, 1000, 2))
        .RuleFor(p => p.Currency, "€")
        .RuleFor(p => p.Quantity, f => f.Random.Int(0, 1000))
        .RuleFor(p => p.Rating, f => f.Random.Float(0, 1))
        .RuleFor(p => p.ReleaseDate, f => f.Date.Past(2))
        .RuleFor(p => p.Image, f => f.Image.PicsumUrl())
        .RuleFor(p => p.Reviews, f => reviewFaker.Generate(f.Random.Int(0, 1000))
    )

图片

在页面中间,有一个仪表板,我们在其中使用了本文介绍的过滤器,分析器和方法。在顶部的搜索框中键入一些文本时,将建议相关产品,并且仪表板内容将根据搜索文本进行更新。

结论

在本文中,我向您展示了如何使用Elasticsearch对复杂的实际场景进行有效的处理,分析和搜索数据。希望我对这个话题感兴趣。

此处提供了带有本文中使用的代码的示例项目。

如何在ASP.NET Core中集成ElasticSearch

Posted on 2021-04-25 | In 技术

查看原文

图片

我敢打赌,您肯定会被要求向Web应用程序中添加高级搜索功能,而且通常是全文的类似Google的搜索。

在技​​术电子商务的开发过程中,我们被要求允许用户对产品进行高级研究,以便他们可以高效,完全地找到所需的内容。

我们基于对象的所有字段上给定字符串的搜索尝试了自定义搜索的实现。为了优化时间,我们尝试在服务和数据库级别之间添加一个缓存层,以避免对数据库造成过多压力,但是我们对结果不满意。然后,我们在市场上搜索了可以满足我们需求的第三方产品,经过深入分析,我们选择采用ElasticSearch:一个基于REST协议的,可管理研究和分析的分布式,易于适应的搜索引擎同样,也方便了数据的外推和转换。

具体来说,我们正在谈论基于Apache Lucene的开源全文搜索引擎,该引擎可用于管理文档的索引和研究。让我们尝试了解基本概念。

ElasticSearch将数据存储在一个或多个索引中。ES的索引与SQL DB的索引非常相似,因为我们使用它来存储和读取文档。

文档是ElasticSearch世界的主要实体。它由一组具有名称和一个或多个值的字段组成。每个文档可能具有一组字段,并且没有给出任何架构或定义的结构。这只是一个JSON对象。

所有文档在存储之前都经过分析。这种分析过程(称为映射)是通过过滤数据内容(例如,删除HTML标签)并将其标记化来执行的,以便将文档拆分为标记。

ElasticSearch中的每个文档都有一个类型。这样就可以将各种文档类型存储在同一索引上,并为几种类型获取几种映射。

ElasticSearch服务器的单个实例称为Node。在很多情况下,单个节点就足够了,但是有时您需要管理故障,或者您有太多数据无法使用单个节点进行管理。在这种情况下,您可以使用多节点集群,这是一组协同工作的节点来管理比单个实例无法处理的更大的负载。您可以配置群集,以便即使某些节点不可用,也可以保证搜索和管理功能。

为了使群集正常运行,ElasticSearch将数据分布在Apache Lucene的多个物理索引上。这些索引称为“ 碎片”,而扩展过程称为“ 碎片”。ElasticSearch自动管理分片,因此最终用户似乎只是一个大索引。

副本是分片的副本,可用于以原始分片的相同模式进行查询。

副本可减轻无法处理所有请求的单个节点上的负载,并提供更高的数据安全性,因为如果您丢失了原始分片中的数据,则可以在副本上对其进行恢复。

ElasticSearch收集了大量有关集群状态,索引设置的信息,并将它们存储到网关中。

从结构上讲,ElasticSearch基于一些简单的关键概念:

  • 默认设置和值使得默认配置足以立即使用ElasticSearch。
  • 它以分布式方式工作。节点自动成为集群的一部分,并且在设置过程中,节点尝试加入集群。
  • 没有SPOF的P2P体系结构(单点故障)。节点自动连接到群集的其他计算机以更改数据和相互监视;
  • 只需在集群中添加新节点,就可以轻松地进行扩展,无论是在数据量上还是在容量上。
  • 在组织索引中的数据方面没有任何限制。允许用户修改数据模型而不会对搜索产生任何影响;
  • NRT(近实时)搜索和版本控制。由于其分布式特性,无法避免延迟和位于不同节点上的数据之间的差异。因此,它提供了版本控制机制。

当ElasticSearch节点启动时,它使用多播(或单播,如果已配置)来查找同一集群中的其他节点并连接到它们。

图片

在群集中,选择一个节点作为主节点。该节点负责管理集群状态和将分片分配给节点的过程。主节点读取集群状态,并在需要时启动恢复模式,该模式允许知道哪些分片可用,并指定其中一个作为主分片。这样,即使群集没有可用的全部资源,它也似乎可以正常工作。然后,主节点查找重复的分片,并将其作为副本处理。

在标准运行期间,主节点检查所有可用节点是否正常工作。如果其中之一在配置的时间范围内不可用,则将该节点视为已损坏,并运行容错过程。容错的主要活动是平衡已损坏节点的群集和碎片,并分配一个负责这些碎片的新节点。然后,对于每个主分片丢失,将定义一个在可用副本之间选择的新主分片。

图片

如前所述,ElasticSearch提供了一些API REST,可供每个能够发送HTTP请求和接收HTTP响应的系统使用(大多数开发框架的所有浏览器和库)。

ElasticSearch请求由一些包含的已定义URL发送。最终是JSON主体。响应也是JSON文档。

ElasticSearch提供了四种索引数据的方式。

  1. 索引API:它允许将文档发送到已定义的索引;
  2. 批量API:它允许通过HTTP协议发送多个文档;
  3. UDP批量API:它允许通过任何协议发送多个文档(更快但更不可靠);
  4. 插件:在节点上执行,它们从外部系统获取数据。

重要的是要记住,索引只是在主分片上而不是在其副本上,因此,如果将索引请求发送到不包含主分片或可能包含其副本的节点,则该请求将转发到主分片。

图片

使用Query API执行搜索。使用查询DSL(基于JSON的语言来构建复杂的查询),可以:

  • 使用各种类型的查询,包括简单查询,短语,范围,布尔值,空间查询和其他查询;
  • 通过组合简单查询来构建复杂查询;
  • 通过排除不符合选定条件的文档而不影响其分数来过滤文档;
  • 查找与其他文件相似的文件;
  • 查找给定短语的建议或更正;
  • 查找与给定文档匹配的查询。

搜索不是一个单阶段的简单过程,但是,通常可以将其分为两个阶段:scatter(分散),在其中查询索引的所有相关分片;gather(收集),在其中收集,处理和排序所有宝贵的结果。

图片

弄脏你的手!

ES提供了云和本地两种使用方式。如果要在Windows计算机上安装它,则需要具有Java虚拟机的更新版本(https://www.elastic.co/support/matrix#matrix_jvm),然后可以从ElasticSearch下载中下载一个zip文件。页面(https://www.elastic.co/downloads/elasticsearch)并将其提取到磁盘上的文件夹中,例如C:\ Elasticsearch。

要执行它,您可以运行C:\ Elasticsearch \ bin \ elasticsearch.bat。

如果要将ElasticSearch用作服务,以便可以使用Windows工具启动或停止它,则需要在文件C:\ Elasticsearch \ config \ jvm.options中添加一行。

对于32位系统,您必须键入-Xss320k*,对于64位系统-Xss1m。*

更改此设置后,您必须打开命令提示符或Powershell并执行C:\ Elasticsearch \ bin \ elasticsearch-service.bat。可用的命令包括安装,删除,启动,停止和管理器。

要创建服务,我们必须输入:C:\ Elasticsearch \ bin \ elasticsearch-service.bat install

要管理服务,我们键入: C:\ Elasticsearch \ bin \ elasticsearch-service.bat管理器,该*管理器 *打开Elastic Service Manager,这是一个GUI,可通过该GUI进行有关服务的自定义设置并管理其状态。

默认cluster.name和node.name是elasticsearch分别和你的主机名。如果您打算继续使用该群集或添加更多节点,则最好通过在elasticsearch.yml文件中对其进行修改来将这些默认值更改为唯一名称。

我们可以通过浏览http:// localhost:9200 /来验证ElasicSearch的正确执行。如果一切正常,我们将得到以下结果:

图片

为了实现基于.NET Core的解决方案,我们使用了NEST软件包,可以通过以下命令安装该软件包:

1
dotnet add package NEST

NEST允许我们在索引和搜索文档以及节点和分片的管理中本地使用所有ElasticSearch功能。
为了管理NEST插件,我们创建了ElasticsearchExtensions类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static class ElasticsearchExtensions
{
    public static void AddElasticsearch(this IServiceCollection services, IConfiguration configuration)
    {
        var url = configuration["elasticsearch:url"];
        var defaultIndex = configuration["elasticsearch:index"];
 
        var settings = new ConnectionSettings(new Uri(url))
            .DefaultIndex(defaultIndex);
 
        AddDefaultMappings(settings);
 
        var client = new ElasticClient(settings);
 
        services.AddSingleton(client);
 
        CreateIndex(client, defaultIndex);
    }
 
    private static void AddDefaultMappings(ConnectionSettings settings)
    {
        settings
            DefaultMappingFor<Product>(m => m
                .Ignore(p => p.Price)
                .Ignore(p => p.Quantity)
                .Ignore(p => p.Rating)
            );
    }
 
    private static void CreateIndex(IElasticClient client, string indexName)
    {
        var createIndexResponse = client.Indices.Create(indexName,
            index => index.Map<Product>(x => x.AutoMap())
        );
    }
}

在其中我们找到对象的配置和映射,在本例中为Product类。在此类中,我们决定忽略在索引阶段存储价格,数量和评级。
通过以下指令在Startup.cs中调用此类:

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddElasticsearch(Configuration);
}

这使我们能够在启动时加载的所有设置,在修改它们elasticsearch的第appsettings.json文件,在其中我们插入如下一行:

1
2
3
4
"elasticsearch": {
        "index": "products",
        "url": "http://localhost:9200/"
}

索引表示选择用来存储文档的默认索引,而url是我们的ElasticSearch实例的地址。
我们的产品对象定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Product
{
public int Id { get; set; }
public string Ean { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Brand { get; set; }
public string Category { get; set; }
public string Price { get; set; }
public int Quantity { get; set; }
public float Rating { get; set; }
public DateTime ReleaseDate { get; set; }
}

如前所述,可以分别或在列表中为产品建立索引。
在我们的产品服务中,我们实现了两种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public async Task SaveSingleAsync(Product product)
{
    if (_cache.Any(p => p.Id == product.Id))
    {
        await _elasticClient.UpdateAsync<Product>(product, u => u.Doc(product));
    }
    else
    {
        _cache.Add(product);
        await _elasticClient.IndexDocumentAsync(product);
    }
}
 
public async Task SaveManyAsync(Product[] products)
{
    _cache.AddRange(products);
    var result = await _elasticClient.IndexManyAsync(products);
    if (result.Errors)
    {
        // the response can be inspected for errors
        foreach (var itemWithError in result.ItemsWithErrors)
        {
            _logger.LogError("Failed to index document {0}: {1}",
                itemWithError.Id, itemWithError.Error);
        }
    }
}
 
public async Task SaveBulkAsync(Product[] products)
{
    _cache.AddRange(products);
    var result = await _elasticClient.BulkAsync(b => b.Index("products").IndexMany(products));
    if (result.Errors)
    {
        // the response can be inspected for errors
        foreach (var itemWithError in result.ItemsWithErrors)
        {
            _logger.LogError("Failed to index document {0}: {1}",
                itemWithError.Id, itemWithError.Error);
        }
    }
}

在这里我们使用_cache数组来进一步缓存产品列表。
对于多模式,我们也实现了批量版本,这使我们能够在更短的时间内索引大量文档,并且我们已经处理了日志插入中的任何错误。

请注意,SaveSingleAsync方法通过检查缓存数组来管理文档的插入和修改。

对于文档删除,我们实现了DeleteAsync方法:

1
2
3
4
5
6
7
8
9
10
public async Task DeleteAsync(Product product)
{
    await _elasticClient.DeleteAsync<Product>
(product);
 
    if (_cache.Contains(product))
    {
        _cache.Remove(product);
    }
}

GetSearchUrl方法允许我们获取用于管理页面调度的URL。
出于开发目的,我们实现了ReIndex方法,该方法允许我们删除索引上的所有文档,然后一次又一次地导入它们。这对于导入现有和未加载文档的列表很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Only for development purpose
[HttpGet("/search/reindex")]
public async Task<IActionResult>ReIndex()
{
    await _elasticClient.DeleteByQueryAsync<Product>(q => q.MatchAll());
 
    var allProducts = (await _productService.GetProducts(int.MaxValue)).ToArray();
 
    foreach (var product in allProducts)
    {
        await _elasticClient.IndexDocumentAsync(product);
    }
 
    return Ok($"{allProducts.Length} product(s) reindexed");
}

出于示例目的,我们创建了一个界面,该界面允许我们通过Bogus插件添加N个动态生成的产品,并管理产品的CRUD。
运行项目后,我们将看到以下屏幕:

图片

例如,如果我们尝试将10种产品添加到索引中,在文本框中输入10,然后单击“ 导入文档”按钮,则可以使用搜索框查看结果,也可以直接从浏览器中浏览到http页面: // localhost:9200 / products / _search,我们将在其中得到这样的结果:

图片

本文中使用的代码可在此处获得。

. 如何在.NET Core中为gRPC服务设计消息

Posted on 2021-04-25 | In 技术

如何在.NET Core中为gRPC服务设计消息

使用协议缓冲区规范定义gRPC服务非常容易,但从需求转换为.NET Core,然后管理服务的演变时,需要注意几件事。

创建gRPC服务的核心是.proto文件,该文件以与语言无关的格式描述了该服务。使用.proto文件,Visual Studio可以为您的服务生成基类(您只需编写特定于业务的代码),或者可以生成用于可靠访问服务的客户端类。

.proto文件必须符合Google的协议缓冲区规范(通常称为ProtoBuf)。原始文件的内容使您可以指定服务的接口。服务接口由两部分组成:

  • 您的gRPC服务提供的方法
  • 这些方法的参数和返回值的数据结构

您可以使用Protocol Buffers规范中定义的标量类型来构建这些数据结构(在ProtoBuf中称为“消息”)。可用的类型包括布尔值,字符串,字节数组和各种数字类型(浮点型,整数型和长型)。没有日期或固定的十进制类型。在接下来的专栏中,我将向您展示如何添加时间戳类型。对于小数,您可以使用float …并伴随着float带来的精度损失。

如果您要开始一个新项目,则要使用自2016年以来的proto3语法。但是,您必须在.proto文件的第一行“非空”行上明确指定proto3标准。引用规范),否则将使用proto2规范解析您的.proto文件。指定您的文件使用proto3看起来像这样:

1
syntax = "proto3";
消息和C#类

使用proto3规范,用于客户信息的消息格式可能如下所示:

1
2
3
4
5
6
7
message CustomerResponse {
int32 custid = 1;
string firstName = 2;
string lastName = 3;
int32 age = 4;
fixed32 creditLimit = 5;
}

等号后的数字指定消息中字段的位置,从位置1开始(在我的示例中,firstName将是消息中的第二个字段)。这些数字在消息中必须是唯一的(即,您不能在同一位置使用两个字段)。您不必按数字顺序列出字段,但是如果您这样做的话,则可以更轻松地发现重复的字段编号(尽管Visual Studio将发现任何重复的编号,并在构建应用程序时将其报告在“错误列表”中)。如果需要,您也可以跳过职位。此定义仅使用奇数,例如:

1
2
3
4
5
message CustomerResponse {
int32 custid = 1;
string firstName = 3;
string lastName = 5;
}

在.NET Core中,消息格式被转换为类,每个字段都成为与消息同名的类的属性。命名这些属性时,.NET Core还将字段名称的第一个字符转换为大写。因此,例如,我上一个示例中的custId字段将成为我代码中CustomerResponse类上的CustId属性。

在此过程中,还得删除字段名称中的所有下划线,并且将以下字母大写(即,Last_name字段名称变为LastName属性)。

该过程还涉及将.NET类型映射到ProtoBuf类型(例如,ProtoBuf int32变为.NET int,ProtoBuf的int64变为long,fixed32变为uint),这需要向.NET Core添加一些新类。例如,ProtoBuf支持字节数组,其类型为字节。名为ByteString的新.NET数据类型支持该字段类型。要加载ByteString,请使用ByteString类的静态CopyFrom方法,并传递一个字节数组,如下所示:

1
2
byte[] bytes = new byte[1000];
cr.Valid = ByteString.CopyFrom(bytes);

要从ByteString检索字节数组,请使用对象的CopyTo方法,并传递要将字节复制到的数组和起始位置:

1
cr.Valid.CopyTo(bytes,0);
数组和字典

您也可以使用【repeated】的关键字将集合包括在定义中(在ProtoBuf中,不是集合的字段称为“单数”)。如果我的客户消息需要一组重复的交易金额,则可以指定如下字段:

1
2
3
message Customer {
int32 id = 1;
repeated fixed32 transactionAmounts = 4;

重复的字段在转换为类的属性时,也使用新的类型:Google.Protobuf.RepeatedField 。例如,我的示例将生成Google.Protobuf.RepeatedField (无符号整数)的属性。您可以使用{}语法来初始化数组,如下所示:

1
2
3
4
CustomerResponse cr = new CustomerResponse
{
CreditLimit = {10, 15, 100}
};

您可能更可能使用其各种Add方法将项目放入集合中:

1
cr.CreditLimit.Add(200);

您可以使用LINQ方法(例如First())或按位置访问RepeatedField中的项目。可以正常工作,例如:

1
uint tranAmount = cr.CreditLimit [1];

ProtoBuf还支持称为map的Dictionary-type集合,该集合允许您为字典的键和值指定类型。我的客户消息可能会使用“友好名称”来跟踪客户的各种信用卡,以定义一个字典,该字典包含密钥(“彼得卡”,“我的旅行卡”)和值(信用卡号)的字符串):

1
2
3
message CustomerResponse {
int32 custId = 1;
map<string, string> cards = 2;

有趣的是,在Visual Studio 2019预览版中,编辑器不会像其他类型一样突出显示map对象(尽管编译得很好)。

相应的属性将为Google.Protobuf.Collections.MapField类型,您可以通过将其Add方法传递给键和一个值来加载它,就像其他任何Dictionary一样。

管理变更

上线后(客户端开始使用它)更改.proto文件相对容易。例如,您可以将具有新位置编号的字段添加到服务器端软件使用的.proto文件中,而不会打扰仍在使用该文件的早期版本的客户端:客户端只是忽略未在其.proto文件中列出的字段。

同样,在相反的情况下(当服务器.proto文件没有客户端的.proto字段具有的字段时),客户端只会发现服务器未发送的属性被设置为其默认值。顺便说一句,在服务器的.proto文件中定义的,未在客户端的.proto文件中定义的字段仍会发送到客户端,但是.NET不能提供一种方便的方式来访问它(至少现在还没有)。

确实,随着服务的发展和修改其.proto文件,您仅应遵守两个规则:

  • 不要更改现有字段的位置编号
  • 不要回收职位编号(即不要用新的字段3替换过时的字段3)

但是,从.proto文件生成的属性不可为空,因此,如果未将属性设置为值,则它将被设置为其默认值。这意味着数字被设置为0;数字被设置为0。将string设置为string.Empty(长度为零的字符串);布尔变成虚假的;ByteString属性默认为ByteString对象,其IsEmpty属性设置为true;并且RepeatedField和MapField属性均默认为其对应的对象,每个对象均不包含任何项目,并且其Count属性设置为0。

由于这种行为,存在从服务的.proto文件中删除字段并且不更新所有客户端(或者只是在服务器上生成响应时未在对象上设置属性)的危险。危险是客户端无法区分未使用的字段和已设置为其默认值的属性之间的区别。如果将我的客户的有效属性设置为false,则客户端将无法确定客户是否无效或服务器是否不再生成该字段。

您可能需要考虑将属性初始化为某个“不合理的”值(例如,数字为-1),以便客户端可以区分设置为默认值的属性和已删除的字段之间的区别。因为这对于布尔值是不可能的(布尔值没有不合理的值),所以您要特别警惕删除(甚至不再使用)布尔类型的字段。

效率和局限性

正如我在较早的概述中所讨论的那样,gRPC服务的功能之一是它们的消息比基于HTTP的(RESTful)服务小得多。如果您真的想利用这种效率,请注意位置1到15仅需要一个字节的额外开销(即超出存储值的数据),而位置16到2047则需要两个字节。将消息格式保持在16位以下似乎是个好主意。

有关将数据打包到尽可能小的空间的选择类型方面的其他效率提示,请参阅规范中的标量类型说明。

顺便说一句,您不能使用以下任何一种作为字段位置编号:负数,0、19,000到19,999(保留给ProtoBuf使用)或大于536,870,911的数字。我是否也可以建议,如果您想使用这些数字,那么您将遇到在本专栏中我无法解决的问题。

真的。别那样做。

在生产环境下处理EFCore数据库迁移–第1部分

Posted on 2021-04-25 | In 技术

在生产环境下处理EFCore数据库迁移–第1部分

安德鲁·洛克(Andrew Lock)撰写了精彩的系列文章《在ASP.NET Core中的应用程序启动时运行异步任务》,其中他以“迁移数据库”为例,介绍了您可以在启动时执行的操作。

在他该系列的第3部分中,他介绍了为什么在启动时迁移数据库并不总是最佳选择。我决定编写一系列有关可以安全迁移数据库的不同方法的系列文章,即使用Entity Framework Core(EF Core)更改数据库的架构。

这是本系列的第一部分,介绍如何创建迁移,而第二部分则介绍如何将迁移应用于数据库,特别是生产数据库。在撰写本文时,我使用了与安德鲁类似的方法,即,我尝试使用EF Core的优点和缺点,一路介绍创建迁移脚本的所有方式。

注意:Andrew和我彼此认识,因为我们同时在为Manning Publications撰写书籍:Andrew的书是“ ASP.NET Core in Action,而我写的书是“ Entity Framework Core in Action) ”。我们分享了当作家的辛劳和喜悦,但是安德鲁在ASP.NET Core方面的工作更加艰辛–他的书长700页,而我的书“只有” 500页。

TL; DR –创建迁移的摘要

注意:单击链接可直接转到涵盖该点的部分。

  • 可以将两种类型的迁移应用于数据库:

    • 添加新的表,列等,称为不间断的更改(简单)。
  • 更改列/表并需要复制数据,称为重大更改(困难)。

  • 有五种方法可以在EF Core中创建迁移

    • 使用EF Core创建迁移-简单,但不能处理所有可能的迁移。
    • 使用EF Core创建迁移,然后手动修改迁移-中到难,但处理所有可能的迁移。
    • 使用第三方迁移构建器来编写C#迁移-很难,因为您需要自己编写迁移,但是您不需要了解SQL。
    • 使用SQL数据库比较工具比较数据库并输出SQL更改脚本–很简单,但是您确实需要对SQL有一定的了解。
    • 通过复制EF Core的SQL来编写自己的SQL迁移脚本 –很难理解,可以很好地控制,但您确实需要了解SQL。
  • 如何确保您的迁移有效–使用CompareEfSql工具。

设置场景–关于创建迁移,我们应该问什么问题?

有很多迁移数据库模式的方法,在开发中,几乎可以使用任何方法。但是,当涉及到迁移生产数据库(即实际用户正在使用的数据库)时,它就变得非常严重。弄错了,至少会给您的用户带来不便,并且更糟的是,甚至会丢失您数据库中的(宝贵)数据!

在获得更新数据库模式部分之前,我们需要构建迁移脚本,该脚本将包含模式以及可能的数据更改。要构建适当的迁移脚本,我们需要问自己一些有关需要应用到数据库的更改类型的重要问题。所需的迁移将是:

  1. 一个非重大更改,也就是说,它只是增加了新的栏目,表格等,这可能而旧的软件仍然运行应用,即旧的软件将与迁移后的数据库一起运行。
  2. 一个重大更改,即有些数据必须复制或迁移过程中转化,无法应用,而旧的软件,即旧的软件会遇到与迁移后的数据库错误(中断服务)。

本文中也介绍了我们正在使用EF Core,它带来的一些好处和限制。好处是,在大多数情况下,EF Core可以自动创建所需的迁移。

约束条件是应用迁移后的数据库必须与EF Core通过查看您的DbContext和映射的类建立的数据库软件模型匹配–我指的是带有大写M的EF Core模型,因为存在一个名为DbContext中的模型,其中包含类和数据库之间的完整映射。

注意:我将介绍迁移,在这些迁移中,您可以控制映射到数据库的类的控制和EF Core配置-有时也称为代码优先方法。

我不会介绍另一种替代方法是,您直接控制数据库,并使用称为 scaffolding 的EF Core命令为您创建实体类和EF Core配置。采用这种方法迁移很简单–只需重新搭建数据库即可。

第1部分。创建迁移脚本的五种方法

正如我在上一节中所述,我们创建的任何迁移脚本都必须将数据库迁移到与EF Core Model匹配的状态。例如,如果迁移在表中添加了新列,则映射到该表的实体类必须具有与该新列匹配的属性。如果数据库架构的EF Core的模型确实与数据库匹配,则您可能会在查询或写入中发生错误。如果迁移脚本与该数据库的EF Core模型匹配,则将其称为创建“可用”数据库。

毫无疑问,EF Core创建的迁移的有效性– EF Core创建了它,因此它将是有效的。但是,如果我们需要编辑迁移,或者我们自己进行迁移构建,那么我们需要非常小心,就EF Core而言,迁移会创建一个“可用”数据库。这是我考虑过很多的事情。

这是创建迁移脚本的方法的列表。

  • 创建C#迁移脚本
    1. 标准EF Core迁移脚本:使用EF Core的Add-Migration命令创建C#迁移脚本。
    2. 手动修改的EF Core迁移脚本:使用EF Core的Add-Migration命令创建C#迁移脚本,然后对其进行手动编辑以添加EF Core遗漏的位。
    3. 使用第三方迁移构建器,例如FluentMigrator。这样的工具使您可以用C#编写自己的迁移脚本。
  • 创建SQL迁移脚本。
    1. 使用SQL数据库比较工具。它将最后一个数据库架构与EF Core创建的新数据库架构进行比较,并生成一个SQL脚本,该脚本会将旧数据库迁移到新数据库架构。
    2. 编写自己的SQL迁移脚本。称职的SQL编写者可以通过捕获SQL EF Core用来创建数据库的方式来编写SQL迁移脚本。

这是一个摘要图,可让您对这五种方法进行总体回顾,并就其易用性和局限性提出个人看法。

img

现在,让我们依次看一看。

1a。标准EF Core C#迁移脚本

这是EF Core提供的标准迁移技术。Microsoft官方文档中提供了充分的文档记录,总而言之,您运行了一个名为Add-Migration的命令,该命令将三个C#文件添加到您的应用程序,其中包含使用Add-Migration 迁移现有数据库以匹配当前EF Core设置/配置所需的更改。

好处 ·自动构建迁移 ·无需学习SQL ·包括还原迁移功能
坏处
局限性 标准迁移无法处理重大更改(但请参见1b)。 不处理SQL功能,例如SQL用户定义的函数(但请参见1b)。
提示 运行“添加迁移”方法时请注意错误消息。如果EF Core检测到可能丢失数据的更改,它将输出一条错误消息,但仍会创建迁移文件。您必须更改迁移脚本,否则将丢失数据–请参阅第1b节。 ·如果您的DbContext在另一个注册了DbContext的程序集中,则需要在构建中使用MigrationsAssembly方法,并且很可能需要在DbContext程序集中实现IDesignTimeDbContextFactory。
结论 这是处理迁移的一种非常简单的方法,并且在许多情况下效果很好。问题是,如果迁移无法满足您的需求,将会发生什么情况。幸运的是,有很多方法可以解决这个问题。

参考:Microsoft的有关创建迁移的文档。

1b。手工修改的EF Core C#迁移脚本

关于EF Core的Add-Migration命令的好处是,它以C#迁移文件为起点,但是您可以自己编辑这些文件以添加代码来处理重大更改或添加/更新数据库的SQL部分。Microsoft提供了通过复制数据处理重大更改的示例。

好处 与标准迁移相同+ ·能够自定义迁移。 ·能够包含SQL功能,例如SQL用户定义的功能。
坏处 ·您需要了解数据库中正在隐藏的内容。 ·可能难以决定如何编辑文件,例如,您是否保留了EF Core的所有内容,然后对其进行了更改,还是删除了EF Core部件并自己完成了?
局限性 没有简单的方法来检查迁移是否正确(但请参阅稍后的CompareEfSql)。
提示 与标准迁移相同。
结论 非常适合进行较小的更改,但由于经常将C#命令与SQL混合使用,因此进行较大的更改可能很困难。这就是为什么我不使用EF Core迁移的原因之一。

参考:Microsoft手动修改迁移的示例。

1c。使用第三方C#迁移构建器

安德鲁·洛克(Andrew Lock)向我指出了一种使用FluentMigrator编写迁移的方法。这与EF迁移的工作原理类似,但是您必须完成详细说明更改的所有艰苦工作。好消息是FluentMigrator的命令非常明显。

好处 不需要学习SQL。 能明显的看到更改了什么,即“代码作为文档”。
坏处 ·您必须确定自己所做的更改。 不保证产生“正确的”迁移(但请参阅稍后的CompareEfSql)。
局限性 - 没有 -
提示 请注意,FluentMigrator有一个“ Migration Runners”,可以将更新应用于数据库,但也可以输出SQL脚本。
结论 我自己没有真正的经验。感觉这是EF Core迁移的一种更清晰的语法,但是您必须自己完成所有工作。

参考:GitHub的FluentMigrator。

2a。使用SQL数据库比较工具

img

有免费的和商业的工具可以比较两个数据库并创建一个SQL更改脚本,该脚本将旧数据库架构迁移到新数据库架构。

Visual Studio 2017(所有版本)中的“视图”选项卡下内置了一个名为“ SQL Server Object Explorer ”的“免费”比较工具。如果右键单击数据库,则可以访问“比较模式”工具(请参见右图),该工具可以生成SQL更改脚本。

在SQL Server的对象资源管理器工具是非常好的,但是没有这个(可惜)多文档。其他商业系统包括Redgate的SQL Compare。

好处 为您构建正确的SQL迁移脚本。
坏处 ·您需要对数据库有一点了解。 ·并非所有的SQL比较工具都生成还原脚本。
局限性 不处理重大更改-需要人工输入。
提示 请注意SQL比较工具,该工具可以输出日光下的所有设置,以确保设置正确。EF Core的迁移非常简单,例如“ CREATE TABLE…”,因此应该这样做。如果您有任何特定设置,则将它们构建到数据库create中。
结论 我在难以手动编码的大型迁移中使用了SQL Server对象资源管理器。对不熟悉SQL语言的人非常有用,尤其有用。

2b。手工编码SQL迁移脚本

这听起来确实很困难-编写自己的SQL迁移,但是手头上有很多帮助,无论是来自SQL比较工具(参见上文),还是查看SQL EF Core用于创建数据库的帮助。这意味着我可以查看并复制以构建结论SQL迁移脚本的SQL。

好处 完全控制数据库结构,包括EF Core不会添加的部分,例如用户定义的函数,列约束等。
坏处 ·您必须了解基本的SQL,如CREATE TABLE等。 ·您必须确定自己所做的更改(但有帮助) ·不能进行自动还原迁移。 ·不保证产生“正确的”迁移(但请参阅稍后的CompareEfSql)。
局限性 - 没有 -
提示 ·我使用一个单元测试来捕获EF Core的确保创建方法的日志输出。那让我得到了实际的SQL EF Core输出。然后,我寻找最后一个数据库的差异。这使得编写SQL迁移更加容易。 ·通过应用所有迁移(包括新迁移)创建数据库,然后运行CompareEfSql来检查数据库是否与EF Core的当前数据库模型匹配,从而对迁移进行单元测试。
结论 这是我使用的,在CompareEfSql工具的帮助下。如果EF Core的迁移功能非常好,为什么还要处理所有这些麻烦呢?这是结论原因: ·完全控制数据库结构,包括EF Core不会添加的部分,例如用户定义的函数,列约束等。 ·由于我正在编写SQL,因此使我考虑了数据库的各个方面。更改–该属性是否可以为空?我需要索引吗?等 ·通过手动修改EF Core的迁移系统来应对重大变化并非易事。我还是坚持使用SQL迁移。 这是针对想要完全控制和可视化迁移的开发人员的。

您可以捕获EF Core的SQL输出以创建数据库,但是可以在调用方法 EnsureCreated ( EnsureCreated 方法用于创建单元测试数据库)时捕获EF Core的日志记录。因为为EF Core设置日志记录有些复杂,所以我在EfCore.TestSupport库中添加了辅助方法来处理该问题。这是一个示例单元测试,它创建一个新的SQL数据库并捕获EF Core生成的SQL命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[RunnableInDebugOnly]
public void CaptureSqlEfCoreCreatesDatabaseToConsole()
{
//SETUP
var options = this.CreateUniqueClassOptionsWithLogging<BookContext>(
log => _output.WriteLine(log.Message));
using (var context = new BookContext(options))
{

//ATTEMPT
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
}
}

让我们看一下这段代码的每一行

  • 第5行。这是一个EfCore.TestSupport方法,为您的DbContext创建选项。此版本使用包含类名的数据库名称。我这样做是因为xUnit测试类是并行运行的,所以我想要此单元测试类的唯一数据库。
  • 第6行。我使用以… WithLogging结尾的选项生成器的版本,该版本允许我捕获日志输出。在这种情况下,我将日志的Message部分直接输出到单元测试输出窗口。
  • 第11和12行。首先,我确保删除数据库,以便在我调用确保创建时,将使用由当前DbContext的配置和映射的类定义的架构来创建一个新的数据库。

以下是在单元测试输出中捕获的部分输出。这为您提供了EF Core用于创建整个架构的确切SQL。您确实只需要提取与迁移有关的部分,但是至少您可以将所需的部分剪切并粘贴到SQL迁移脚本中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE DATABASE [EfCore.TestSupport-Test_TestEfLogging];
Executed DbCommand (52ms) [Parameters=[], CommandType='Text', CommandTimeout='60']
IF SERVERPROPERTY('EngineEdition') <> 5
BEGIN
ALTER DATABASE [EfCore.TestSupport-Test_TestEfLogging] SET READ_COMMITTED_SNAPSHOT ON;
END;
Executed DbCommand (5ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [Authors] (
[AuthorId] int NOT NULL IDENTITY,
[Name] nvarchar(100) NOT NULL,
CONSTRAINT [PK_Authors] PRIMARY KEY ([AuthorId])
);
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
CREATE TABLE [Books] (
[BookId] int NOT NULL IDENTITY,
[Title] nvarchar(256) NOT NULL,
-- rest of SQL left out

如何确保您的迁移有效–使用CompareEfSql工具

在创建迁移的描述中,我曾多次提到CompareEfSql。该工具将数据库与EF Core首次用于DbContext时创建的数据库模型进行比较。通过DbContext实例中的Model属性访问此模型,是通过查看DbContext配置以及DbSet和DbQuery属性来构建结论EF Core的。

这使开发人员可以根据EF Core Model测试现有数据库,并在错误消息不同的情况下为您提供错误消息。我发现这是一个非常强大的工具,它使我可以手动编码SQL迁移,并确保它们是正确的有一些小限制。这是一个示例单元测试,如果数据库架构与EF Core的模型不匹配,该测试将失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Fact]
public void CompareViaContext()
{
//SETUP
var options = … options that point to the database to check;
using (var context = new BookContext(options))
{
var comparer = new CompareEfSql();

//ATTEMPT
//This will compare EF Core model of the database
//with the database that the context's connection points to
var hasErrors = comparer.CompareEfWithDb(context);

//VERIFY
//The CompareEfWithDb method returns true if there were errors.
//The comparer.GetAllErrors property returns a string
//where each error is on a separate line
hasErrors.ShouldBeFalse(comparer.GetAllErrors);
}
}

我喜欢这个工具,它位于EFCore.TestSupport开源库中。它使我能够构建迁移,并确保它们能够正常工作。我也将其作为正常的单元测试来运行,它会立即告诉我是否是我或另一个同事更改了EF Core的设置。

您可以在名为EF Core的文章中获得对该工具的更详细的描述:完全控制数据库模式及其许多功能和配置可以在CompareEfSql文档页面中找到。

注意:我最初是为EF6.x构建此版本的(请参阅此旧文章),但是由于EF6.x并未完全公开其内部模型而受到限制。

有了EF Core,我可以做更多的事情,现在我可以检查几乎所有内容,并且因为我利用了EF Core的脚手架服务,所以它适用于EF Core支持的任何数据库。

结论–第1部分

本系列的这一部分将介绍如何创建有效的迁移,而第二部分则涉及将迁移应用于数据库。本文列出了使用EF Core时用于创建数据库迁移的所有适用方法-优缺点。如您所见,EF Core的Add-Migration命令确实很好,但是并不能涵盖所有情况。

由您决定要遇到的迁移类型,以及您希望对数据库架构进行何种级别的控制。如果您仅使用EF Core的标准迁移(1a)就可以摆脱困境,那么这将使您的生活更轻松。但是,如果您预期会发生重大变化,或者需要设置额外的SQL功能,那么您现在知道可用的选项。

令人担心的部分出现在part2中-将迁移应用于生产数据库。更改包含关键业务数据需求(需求!)的数据库,请仔细计划和测试。您需要考虑如果(何时!)迁移因错误而失败时该怎么办。

我放弃EF6中的EF迁移的最初原因是它在启动时自动迁移运行良好,直到它在部署时引发错误!确实很难找到迁移中的错误-仅此一项就使我远离使用EF迁移(那时候回想起了这篇老文章)。

EF Core的迁移处理要比EF6更好:已可以实现自动迁移(感激!),并且EF Core迁移对git-merge更加友好,仅提及两个更改。但是,我构建SQL迁移脚本的方式使我比正在运行Add-Migration时要更加仔细地思考自己在做什么。EF Core是一个非常出色的O / RM,但有时确实有许多隐藏功能。

创建SQL迁移脚本使我从数据库的角度考虑了迁移问题,而且我经常会对数据库和C#代码的一些细微调整,以使数据库更好的运行。

在生产中处理EFCore-模式实战

Posted on 2021-04-25 | In 技术

在生产中处理EFCore-模式实战

这是使用EF Core迁移数据库的系列文章中的第二篇。本文着眼于将迁移应用于数据库,并从第1部分开始,该部分介绍了如何创建迁移脚本。如果您还没有阅读第1部分,那么本文的某些部分将毫无意义,因此这里是第1部分的快速回顾。

  • 可以将两种类型的迁移应用于数据库:
    • 添加新的表,列等,称为不间断的更改(简单)。
    • 更改列/表并需要复制数据,这称为重大更改(困难)。
  • 将迁移应用于数据库的主要方法有两种
    • 使用EF Core迁移功能
    • 使用EF Core创建迁移,然后手动修改迁移。
    • 使用第三方迁移构建器在C#中编写迁移。
    • 使用SQL数据库比较工具比较数据库并输出SQL更改脚本。
    • 通过复制EF Core的SQL编写自己的SQL迁移脚本。

因此,既然您现在知道如何创建迁移脚本,那么我将研究可以将迁移应用于生产数据库的不同方式,以及这些方式所具有的利弊。

TL; DR –内容摘要

注意:单击链接可直接转到涵盖该点的部分。

  • 您具有影响迁移方法可以使用的应用程序的类型。
  • 您必须考虑可能发生的错误并制定计划。
  • 有四种方法可以将迁移应用于数据库
    • 在启动时调用context.Database.Migrate()Very Easy,但是存在一些严重的问题,限制了它的实用性。
    • 通过控制台应用程序调用context.Database.Migrate()-Easy,并且效果很好,尤其是在部署管道中
    • 将EF Core迁移输出为SQL脚本并在目标数据库上执行该脚本 -Hard,但是却能提供很好的控制。
    • 使用数据库迁移应用程序工具来应用您自己的SQL脚本 -Hard,但是您却可以很好地控制。
  • 应用迁移的三个不同级别。
    • 在迁移数据库时停止应用程序是最安全的选择,但并非总是可能的。
    • 在应用程序运行时,可以将某些(但不是全部)不间断的更改应用于数据库。
    • 对于连续服务应用程序(7*24小时运行的服务),要应用重大更改需要五个步骤。

场景分析–您的产品是哪种应用程序?

在第1部分中,我们着重于创建“有效”的迁移,以及迁移是不间断的变更还是重大变更(请参阅本文开头的快速定义,或第1部分中的此链接。

现在,我们正在考虑将迁移应用于数据库,但是我们拥有的选项取决于正在访问数据库的一个或多个应用程序。这是您需要考虑的问题。

  1. 是只有一个应用程序访问该数据库,还是您的应用程序是横向扩展的Web应用程序,即,同时运行多个版本的应用程序。如果您的应用程序是横向扩展,则将删除其中一个选项。
  2. 您可以在将迁移应用到数据库时停止应用程序,还是您的应用程序提供7*24小时连续服务?在应用重大变更方面,更新连续服务应用程序会带来一些挑战。

在迁移生产数据库时,有点偏执是可以的。

正如我在第1部分末尾所说的那样-当您将迁移应用于生产数据库时,最恐怖的部分到来了。更改包含关键业务数据需求(需求!)的数据库,请仔细计划和测试。您需要考虑如果(何时!)迁移因错误而失败时该怎么办。

在考虑应用迁移的不同方法时,您应该脑海中浮现“如果有错误会发生什么?”。这可能会促使您采用更复杂的迁移方法,因为它更易于测试或还原。我不能为您提供规则或建议,因为每个系统都不同,但是对故障有点偏执并不是一件坏事。我应该让您构建一个更健壮的用于迁移应用程序及其数据库的系统。

第2部分:如何将迁移应用于数据库。

下面的列表提供了将迁移应用于数据库的不同方法。我列出了EF Core案例的三个选项:第一个是最简单的,但是它有其他两个选项所没有的限制。SQL迁移没有实际限制,但确实需要数据库迁移应用程序工具才能以正确的顺序应用SQL脚本。

这是您可以应用迁移的方法列表。

  1. EFCore迁移
    1. 在启动时调用context.Database.Migrate()
    2. 通过控制台应用程序或管理命令调用context.Database.Migrate()
    3. 将迁移输出作为SQL脚本输出,然后在目标数据库上执行该脚本。
  2. SQL迁移
    1. 使用数据库迁移应用程序工具。

最后,如何应用迁移取决于迁移类型(中断或不中断)和要更新的应用程序类型(单个应用程序,并行运行的多个应用程序或必须停止的应用程序)。这是所有这些排列的图表。

img

外部的深蓝色表示可以在所有情况下都应用SQL迁移,而内部较浅的方框表示可以在其中添加不同类型的EF Core迁移。以下是有关该图的一些澄清说明:

  • 该图显示了标准EF迁移和手工修改的EF迁移,但是当我谈论应用迁移时,两者之间没有区别-我们很简单地应用EF Core迁移。
  • 图中的“五个阶段的应用程序更新”红色框表示您需要对无法停止的应用程序进行重大更改所需要的复杂阶段。我将在文章末尾介绍。

现在,我将详细介绍应用迁移的每种方式。

1a。在启动时调用context.Database.Migrate()

到目前为止,这是应用迁移的最简单方法,但是它有一个很大的局限性–您不应同时运行Migrate方法的多个实例。如果横向扩展Web应用程序,则可能会发生这种情况。引用安德鲁·洛克(Andrew Lock)的话:“ 我们不能保证这会给您带来麻烦,但是除非您非常谨慎地确保幂等更新和错误处理,否则您很可能会陷入困境 ” –请参阅他的帖子的这一部分“ 在ASP.NET Core中的应用启动时运行异步任务 ”。

好处 ·相对容易实现(请参阅提示) ·确保在应用程序运行之前数据库是最新的。
坏处 ·不得并行运行两个或多个Migrate方法。 ·如果迁移有错误,则您的应用程序将不可用。 ·难以诊断启动错误
局限性 不适用于连续服务系统
提示 我非常喜欢Andrew Lock的文章中的在启动时运行迁移的选项。我在一些使用内存数据库的演示系统中使用了类似的方法,这些数据库需要初始化(请参见本示例)
我的建议 如果您正在运行单个Web应用程序或类似的Web应用程序,并且可以在没有人使用它的情况下更新系统,那么这可能对您有用。我没有像我使用的许多系统那样使用横向扩展。

1b。通过控制台应用程序或管理命令调用context.Database.Migrate()

如果您不能并行运行多个Migrate方法,那么确保此方法的一种方法是在设计为仅执行Migrate方法的独立应用程序内调用Migrate方法。您可以在主Web应用程序解决方案中添加一个控制台应用程序项目,该项目可以访问DbContext并可以调用Migrate。您既可以自己运行它,也可以让您的部署系统运行它(EF6.x用户注意–这等效于运行Migrate.exe,但其中已编译应用程序dll)。

好处 ·它适用于所有情况。 ·与部署系统配合良好。
坏处 还有更多工作。
局限性 –无–,但请注意持续进行的五阶段应用程序更新
提示 如果您的控制台应用程序使用连接字符串来定义要将迁移应用到哪个数据库,那么它将更易于在部署管道中使用。
我的建议 如果您具有部署管道,那么这是一个不错的选择,因为您可以在部署过程中执行控制台应用程序。如果您是手动应用迁移,则有命令Update-Database。

1c。将EF Core迁移转换为脚本并将其应用于数据库

通过使用脚本迁移命令EF Core会将特定的迁移或默认情况下的所有迁移转换为SQL脚本。然后,您可以使用可以在要更新的特定数据库上执行SQL的方法来应用此方法。您可以在SQL Server Management Studio中手动执行SQL ,但是通常您的发布管道中有一些内容可以在适当的时间执行。

好处 ·它适用于所有情况。 ·与可以使用SQL脚本的部署系统一起很好地工作。 ·您可以在运行SQL之前先查看它,看看它是否正常。
坏处 ·比控制台应用程序(1b)更多的工作 ·您需要一些应用程序将脚本应用于正确的数据库。
局限性 –无–,但请注意持续进行的五阶段应用程序更新
提示 SQL包含用于更新迁移历史记录的代码,但是您必须在Script-Migration命令中包括idempotent选项,以获取阻止两次应用迁移的检查。
我的建议 如果您想使用EF Core的Migrate方法,那么我建议您使用控制台应用程序1b。它与使用脚本一样安全,并且执行相同的工作。但是,如果您的管道已经可以使用SQL更改脚本,那么这非常适合您。

2a。使用迁移工具应用SQL脚本

如果创建了一系列SQL迁移脚本,则需要以下步骤:a)以正确的顺序应用它们,b)仅应用一次。EF Core的迁移包含执行“正确顺序”和“仅一次”规则的代码,但是当我们编写自己的迁移脚本时,我们需要一个可以提供这些功能的工具。

我和其他许多人使用了一个名为DbUp的开源库,该库提供了这些功能(以及更多功能),还支持多种数据库类型。我按字母顺序排列迁移脚本,例如“ Script0001 –初始迁移”,“ Script0002 –添加种子数据”以供DbUp应用。就像EF Core迁移一样,DbUp使用一个表来列出哪些迁移已应用到数据库,并且仅在该表中没有迁移时才应用。

还可以使用其他迁移工具,例如Octopus Deploy和各种RedGate工具(但我没有使用过它们,因此请检查它们是否具有正确的功能)。

好处 ·它适用于所有情况。 与部署系统配合良好。
坏处 ·您必须管理脚本。
局限性 –无–,但请注意持续进行的五阶段应用程序更新
*提示 *(适用于DbUp) ·我制作了一个控制台应用程序,该应用程序接受连接字符串,然后运行DbUp,因此可以在部署管道中使用它。 ·为了进行测试,我使运行DbUp的方法在“仅以调试模式运行”单元测试中可用于我的单元测试程序集,该方法使用我的CompareEfSql工具正确迁移了本地数据库(请参阅本系列第1部分中有关测试迁移的部分。
我的建议 使用EF Core的项目上使用这种方法。

应用程序和应用程序迁移

将迁移应用于数据库时,可以停止应用程序,或者在某些情况下可以在迁移运行时应用迁移。在本节中,我将介绍为您提供的不同选项。

1.在迁移数据库时停止应用程序

这是最安全的选项,可与重大更改和不中断更改一起使用,但是您的用户和您的业务可能并不那么满意。我称其为“维护站点”。在“站点关闭”方法中,您不想在用户输入数据或完成订单时停止应用程序。这就是您或您的公司获得不良声誉的方式。

我早在2015年就遇到了这个问题,并且我创建了一种方法来警告人们该网站将要关闭,然后停止除管理员以外的所有人员访问该应用程序。我之所以选择这种方法,是因为对于正在使用的Web应用程序,此方法比支持破坏性更改同时保持Web应用程序运行的开销要小(我将在稍后介绍对连续服务应用程序进行中断)。通常在周末和晚上,您可能会遇到所使用服务的“此站点已关闭维护”。

注意:我写了一篇名为“ 如何使ASP.NET MVC网站“为了维护而停机 ””的文章,您可能希望看一下-该代码是针对ASP.NET MVC5的,因此需要一些工作才能使其正常工作。 .NET Core,但该想法仍然有效。

在应用程序运行时应用不间断的迁移

从理论上讲,通过不间断的更改,您可以在旧应用程序运行时将其应用于数据库,但是有些问题可能会让您失望。例如,如果您添加了一个没有SQL默认值且不知道该新列的旧软件的新的非空列,并尝试插入新行,则您会收到一条SQL错误,因为旧软件没有提供了非空列的值。

但是,如果您知道不间断的迁移没有问题,那么在旧应用程序运行时应用迁移将为您的用户提供连续的服务。有多种方法可以执行此操作,具体取决于您选择了哪种迁移应用程序方法,想到的就是Azure的暂存槽(已经存在了很长时间)和更新的Azure Pipelines。

将重大更改应用于连续运行的应用程序:五阶段的应用程序更新。

最困难的工作是对不断运行的应用程序进行重大更改。在显示不同方法的图表中,右上方会显示一个名为“五阶段应用程序更新”的红色框。该名称来自以下事实:您需要分阶段迁移,通常为五个阶段,如下图所示。

注意:安德鲁·洛克(Andrew Lock)称赞我在上一节中描述的“添加不可为空的列”问题可以分三个阶段处理:a)添加新列但可为空,b)部署已知该列的新软件,以及c)将列更改为不可为空。

这是我的《EFCore》一书的第11.5.3节中的图表,该图显示了添加重大更改所需的五个阶段,这些更改将现有的CustomerAndAddress表分为两个表,Customers和Addresses。

[img

如您所见,这样的更新创建起来很复杂,应用起来也很复杂,但这就是运行连续系统的成本。这五个阶段没有任何真正的替代方案,除了您永远不要对连续运行的系统应用重大更改(我听说有人说这是他们的方法)。

注意:我在我的书“ Entity Framework Core in Action ”的11.5.3节中介绍了持续的,五个阶段的应用程序更新,您还可以在Neil Ford的“ Building Evolutionary Architectures ” 一书的第5章中找到有关此内容的内容。等。

结论

如果数据库中的数据和服务的可用性对组织很重要,那么您必须认真对待数据库迁移。在第1部分中,我介绍了创建迁移脚本的不同方法,并且本文介绍了如何将这些迁移应用于生产数据库。本系列文章的目的是为您提供各种选择,以及它们的优缺点,以便您可以就如何处理迁移做出明智的决定。

就像我在第一篇文章中所说的那样,我与EF迁移的第一个磨合是使用EF6。我非常了解EF6,并且写过《 Entity Framework Core in Action》一书,我对EF Core的了解甚至更好。围绕迁移从EF6到EF Core的变化代表了EF Core中整个方法的变化。

EF6进行了很多“魔法”操作,使其更易于使用- 启动时自动迁移就是其中之一。问题是,当EF6的“魔法”效果不佳时,很难对其进行梳理。EF Core的迁移方法是由您决定如何在何处以及如何使用它-没有自动的“魔法”。EF Core迁移的许多其他小变化来自于聆听EF4到6的用户。

因此,在生产数据库上的迁移令人恐惧,且始终令人恐惧。我已经为您提供了一些有关选项的见解,但这仅是更改生产数据库的最低要求。需要根据需要添加备份,策略,产品前测试和部署管道,以构建可靠的系统。

祝你能享受编码的快乐!

Untitled

Posted on 2021-04-25

title: 如何编写高质量的C#代码(一)
date: 2020-6-20 21:05
tags: 技术
author: 邹溪源
categories:

  • 技术

如何编写高质量的C#代码(一)

从”整洁代码“谈起

一千个读者,就有一千个哈姆雷特,代码质量也同样如此。

想必每一个对于代码有追求的开发者,对于“高质量”这个词,或多或少都有自己的一丝理解。当我在长沙.NET技术社区群抛出这个问题时,众说纷纭。有人说注释齐全、可读性高,就是高质量;有人说变量命名、代码层次清晰,就说高质量的代码;有人说那些使用了新特性的代码,很多都是高质量代码;也有人说,高质量的代码是个伪命题,因为他往往要花大量的精力才能精心打磨,有这个时间,产品早就黄了。

说到”高质量“代码,就不得不提”整洁代码”。这个概念来源于畅销书《代码整洁之道》(The Clean Code)中,鲍勃大叔引入了这个整洁代码的概念,他认为

写整洁代码,需要遵循大量的小技巧,贯彻艰苦习得的‘整洁感’”,这种“代码感”就说关键所在。有些人生而有之。有的人费点劲才能得到。它不仅让我们看到代码的优劣,还予我们以借戒规之力化优为列的攻略。

缺乏”代码感”的程序员,看混乱是混乱,无处着手,有“代码感”的程序员,能从混乱中看出其他的可能与变化。“代码感”帮助程序员选出最好的方案,并指导程序员指定修改行动计划,按图索骥。

编写整洁代码的程序员就像艺术家,他能够用一系列变化把一块白板变作由优雅代码构成的系统。

这本书值得摆在每一位程序员的案头。许多热衷于英文原作的读者都会说国人翻译的许多作品都失去了原作的韵味,但这本韩磊老师翻译这本中文版十几年过去了,印刷了许多版了,也能客观证明这本译作的价值。

也许初读这本书,许多作者提到的手法我们无法短时间内认真体会,但许多读过这本书都表示,许多想法在我们写代码的时候突然迸溅而出,使得思路能够更加通达,并达到一种“人码合一”的状态。

”代码感“

在我们大部分开发者看来,我们开发的代码,往往无需涉及过于复杂的业务逻辑或底层技术,只需简单的使用一些代码拼凑,即可按时完成我们的任务,也就说所谓的”CRUD业务开发者“。

但业务系统本身也并非全靠所谓的“无代码平台”或“代码生成器”能够自动开发完成,他依然需要开发者用心去设计其中的逻辑、变量、结构、流程,才能更好的运转,尤其是要想让应用系统能够保持长久的生命力,更需要我们能够编写更高质量的代码。

在《代码整洁之道》中,作者将这种编写高质量代码的能力,称为“代码感”,这种感觉有时需要灵光一现,有时又需要花费大量的精力才能完成。

就像在《灌篮高手》中,安西教练让大家培养球感:

image-20200827083520175

两万个球?写两万个类/方法/代码行?确实是一种提高”代码感“的好方法。

但跟投球要掌握方法一样,简单的重复写两万行代码估计很难提高代码质量,依然需要大量刻意练习才能带来质量上的提升。

而如何编写高质量代码,在软件开发领域,也有一些前人总结出来的良好准则,人们将这些准则,总结为“设计原则”。除了设计原则外,还要许多良好的实践模式,人们将它们称为”设计模式“。设计原则就像是内功心法,设计模式,则像招数功夫。

也许我们无法完全遵循这些原则或模式,但能够灵活的运用,总能给代码质量带来提升。

何为高质量代码

我个人认为:高质量代码是可读性强、易于测试,它们能够恰如其份的表达业务的需要,并能根据业务需要易于修改的代码。 高质量的代码也许与技术架构、特定API、特定的语言没有太大关系,但高质量代码或许都具备一些相似的特点。

代码结构

结构是代码的核心,就像高楼的支架,为整个代码的完整运行奠定基础。好的代码一定结构清晰,让人易于理解,并能快速定位问题、解决问题。

有人说好文章的结构特点便是:” 凤头、猪肚、豹尾“, 文章的起头要奇句夺目,引人入胜,如同凤头一样俊美精采;文章的主体要言之有物,紧凑而有气势,如同猪肚一样充实丰满;文章的结尾要转出别意,宕开警策,如同豹尾一样雄劲潇洒。 代码也许无需追求达到这么高的境界,但遵循一定清晰的代码结构也能达到同样的效果。

结构按照我个人的理解,可能包括以下几种层面:1、项目文件夹命名;2、分层;3、模块命名;4、代码格式。

1、项目文件夹

对于复杂项目,打开文件夹和解决方案的第一眼,是清晰还是紊乱,往往就是我们对于项目的第一印象。许多资深研发工程师,都会倾向于用数字来对文件夹进行编号,例如对于复杂项目,我们使用如下命名方式对定义解决方案文件夹,虽然不会花特别多的功夫,但会给开发过程带来许多便利。

当然,由于在Visual Studio中,项目文件夹本身属于sln解决方案文件中定义的层级结构,并不会在资源管理器文件夹中体现,所以有时还需要在资源管理器文件夹中也定义类似的层级结构。

1
2
3
4
5
6
01 基础服务
02 框架服务
03 应用服务
01 工作流服务
02 权限服务
03 日志服务

2、分层

分层式架构大家都习以为常,其中尤其以三层架构(用户表现层,业务逻辑层,数据访问层)已经深入人心,成为许多.NET开发者的普遍认可,而领域驱动设计最常见的则是四层式领域驱动设计(用户界面层,应用层,领域层,基础设施层)。

分层式架构体现了”关注度分离“的原则,在进行软件开发过程中,可以根据需求,找到对应的逻辑分层,进行代码实现;有时不同逻辑分层的组件会以各自不同的发展速度迭代以满足不同的需求;在适当的情况下,还能采用分布式架构,让不同层运行在不同的基础设施中,期间通过rpc等方式保持通信,给架构留下了足够的弹性空间。

设计分层式架构并非越多越好,尽量控制在三到四层就足够了,不然会陷入”千层饼“的陷阱,过多的分层和过少的分层,其实没有任何区别。

对于后端工程师来说,理解分层式架构并不困难,难的是要识别哪里逻辑代码应该归属于哪一层;而许多对于方兴未艾的前端技术来说,如何分层,却似乎并不是一件容易的事,由于前端业务要适应来自用户层面的无穷变化,很容易就陷入“意大利面”式的代码混乱中。vuex框架为前端开发者提供了一种良好的示例,有时无需深入了解vuex的机制,只需”模仿”这种分层方法,就能写出更加易于维护的前端代码了。

3、模块(类库)

模块的设计和耦合性

在.NET开发中,模块有时是一个独立的项目,并以一个独立dll(类库)的形式进行分发。模块也是最为常见的一种代码实践,但在《领域驱动设计·软件核心复杂性应对之道》一书中,作者埃里克·埃文斯却指出模块的运用,引起了“认知过载”的问题:

认知负荷理论认为,在问题解决和学习过程中的各种认知加工活动均需要消耗认知资源,若所有活动所需的资源总量超过个体拥有的资源总量,就会引起资源的分配不足,从而影响个体学习或问题解决的效率,这种问题就说“认知过载”。

这段理论确实有点拗口,对应到软件开发过程中,用通俗的说法,就是这个包承载的知识量太大了,把原本可以分离到多个模块中的逻辑代码都囊括进来,使得其反而降低了开发的效率。

尤其是类库的定义,不同的开发者有不同的习惯,有时按技术来划分,有时又按业务场景来划分,有时分拆,有时组合,“千人千面”,不连贯的设计思想,和“能用就行”的想法混合在一起,很容易就造成了一锅粥的情况。

在.NET项目中,每用一个using,就引入了一种耦合,而使用了new方法,创建了一个对象的示例,又引入了一个对象的耦合。

1
2
3
4
5
6
7
8
9
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using xxx.Core;
using xxx.Infrastructure.Extension;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;

而设计优良的代码模块,则可以让依赖尽可能的减少。

模块其实也是实践“高内聚,低耦合”思想的主要阵地,如果业务相关性很高的对象被划分到不同的模块中,往往会使得开发者很难理解它们在业务上的作用,也会导致模块间的耦合进一步提高。

因此,好的模块设计应该将那些具有紧密概念关系的模型元素集中在一起,并能描述该模型元素的职能,使之成为一个内聚的概念集合。

组件设计的原则

关于如何设计模块,在《敏捷软件开发 原则、模式与实践》一书中,作者引述了以下设计原则基于粒度这个角度为组件的内聚性进行描述:

重用-发布等价原则(REP)

重用的粒度就是发布的粒度。REP指出,一个组件的重用粒度可以和发布粒度一样大。我们所重用的任何东西都必须被发布和跟踪。简单的编写一个类,然后声称它是可重用的做法是不现实的。只有在建立一个跟踪系统,为潜在的使用者提供所需要的变更通知、安全性以及支持后,重用才有可能。

共同重用原则(CRP)

一个组件中的所有类应该是共同重用的,如果重用了组件中的一个类,那么就要重用组件中的所有类。

共同封闭原则(CCP)

组件中的所有类对于同一种性质的变化应该是共同封闭的。一个变化若是对一个封闭的组件产生影响,则将对组件中所有的类产生影响,而对其他组件则不造成任何影响。

从稳定性的角度为组件的内聚性进行描述:

无环依赖原则:

在组件中的依赖关系图中,不允许存在环。

稳定抽象原则

朝着稳定的方向进行依赖。

设计不能是完全静态的。要使设计可维护,某种程度的易变性是必要的。我们通过遵循共同封闭原则来达到这个目标。使用这个原则,可以创建对某些变化类型敏感的组件。这些组件设计为可变的。我们期望他们变化。

稳定抽象原则

组件的抽象程度应该与其稳定程度一致。

4、代码格式

类的基本结构

代码格式,就是一个C#代码文件的逻辑结构。写代码其实是一件成本很低的事,但维护代码,却是一件成本很高的事。开发一个功能,只需短短几十分钟时间,但如果我们要去找出代码中存在的缺陷,却往往需要花费大量的时间。

这就客观上要求,我们书写的代码应该尽量方便阅读(可读性)、检索(快速找问题)、易于维护,而书写出“格式化”的代码,大概是我们能够提高代码质量的第一步。

对于书写的代码,大部分都是从上往下阅读,在需要阅读的代码较多量时,往往会选择折叠到定义,这样就能一眼看出每个方法的用途,要达到这个效果,就意味着我们需要精心设计安排代码的垂直格式。有经验的开发者往往会按照这种结构。

私有字段:定义类内部的基本成员,高层次概念,常量,和引入的算法。

构造函数:定义类的创建过程。

公共方法:定义类为外部暴露的行为。

私有方法:定义类为内部提供的行为。

类的格式要求

在《代码整洁之道》这本书中,作者介绍了他对于代码的格式要求:

垂直格式

代码文件的长度控制在200-500行左右,且短文件通常比长文件易于理解。垂直阅读时,顶部是粗线条概述,隐藏了故事细节,然后再不断展开。

每行展示一个表达式或一个子句,尤其是C#的链式语法,尽量一行代码就是一个方法。

1
2
3
4
entity.Property(e => e.Memo)
.HasMaxLength(500)
.IsUnicode(false)
.HasComment("备注");

每组代码行展示一个完整的思路,思路间用空白行隔开。垂直方向上,靠近的代码可以展示它们之间的紧密关系,能够让代码更好阅读。

变量声明应尽可能靠近其使用位置,因为函数很短,本地变量应该在函数的顶部出现。一个函数调用了另外一个函数,应该把它们放到一起,且调用者应该在被调用者上面。概念相关的代码应该放到一起,相关性越强,彼此之间的距离就该越短。

横向格式

横向首先表现在代码的宽度上,尽量控制在一行代码不超过120个字符。

水平方向上,可以用空格字符把彼此紧密相关的变量或对象连接在一起,也可以用空格将相关性较弱的对象分割开。

注意水平缩进和左对齐,尤其是上面提到的链式语法,如果点号没对齐,简直让人难受。

1
2
3
4
entity.Property(e => e.UserId)
.HasMaxLength(10)
.IsUnicode(false)
.IsFixedLength();
小结

本文对如何编写高质量代码进行了一些简单的概述,介绍了代码的分层、组件(包)的设计、以及整洁代码中的一些开发实践,通过了解这些知识,能够让我们逐渐形成自己对于代码的体会,并通过不断的练习,将能够提高我们的代码能力。

当然,有时,写文档、适当的做一些软件工程设计,看起来与完成代码编写无关,但也同样是提高代码质量的一种手法,通过就像许多好文章往往会先搭框架,好代码也同样如此。

根据业务需要,画一波流程图、领域模型图、类图、时序图能够让我们的思路提前沉淀,让我们的开发过程更加流畅,更能开发出高质量的代码。

下一篇,将对规范命名、注释、设计向量、设计原则、设计模式进行一些讨论。

Untitled

Posted on 2021-04-25

在ASP.NET Core中的应用启动时运行异步任务-第1部分

这是该系列的第一篇文章:在ASP.NET Core中的应用程序启动时运行异步任务。

  1. 第1部分-用于运行异步任务的内置选项(本文)
  2. 第2部分-运行异步任务的两种方法
  3. 第3部分-对异步任务示例和其他可能解决方案的反馈
  4. 第4部分-使用运行状况检查在ASP.NET Core中运行异步任务
  5. 第5部分-在ASP.NET Core 3.0中的应用启动时运行异步任务

有时,您需要先执行一次性初始化逻辑,然后应用才能正常启动。例如,您可能要验证您的配置正确,填充缓存或运行数据库迁移。在这篇文章中,我将介绍可用的选项,并展示一些我认为可以很好解决问题的简单方法和扩展点。

我首先描述使用来运行同步任务的内置解决方案IStartupFilter。然后,我逐步介绍了用于运行异步任务的各种选项。您可以(但可能不应该)使用IStartupFilter或IApplicationLifetime事件来运行异步任务。您可以使用该IHostedService界面运行一次性任务,而不会阻止应用程序启动。但是,唯一真正的解决方案是在program.cs中手动运行任务。在我的下一篇文章中,我将显示一个建议的建议,该建议使此过程更容易一些。

为什么我们需要在应用启动时运行任务?img

在应用能够启动并开始处理请求之前,通常需要运行各种初始化代码。在ASP.NET Core应用程序中,显然有很多事情需要发生,例如:

  • 确定当前的托管环境
  • 从appsettings.json和环境变量加载配置
  • 依赖项注入容器的配置
  • 依赖项注入容器的构建
  • 中间件管道的配置

所有这些步骤都需要执行以引导应用程序。但是,在WebHost运行并开始侦听请求之前,通常要执行一次性任务。例如:

  • 检查您的强类型配置是否有效。
  • 使用数据库或API中的数据启动/填充缓存
  • 在启动应用程序之前运行数据库迁移。(这通常不是一个好主意,但对于某些应用程序可能已经足够了)。

有时,这些任务不具有被运行之前,你的应用程序启动服务请求。例如,缓存启动示例-如果其性能良好,则在启动之前查询缓存是否无关紧要。另一方面,您几乎可以肯定要在应用程序开始处理请求之前迁移数据库!

有一些示例表明ASP.NET Core框架本身需要一次性的初始化任务。数据保护子系统就是一个很好的例子,该子系统用于瞬时加密(cookie值,防伪令牌等)。在应用程序可以开始处理任何请求之前,必须初始化此子系统。为了解决这个问题,他们使用IStartupFilter。

与同步运行任务 IStartupFilterimg

我之前已经写过有关的文章IStartupFilter,因为在您的工具栏中可以使用它来定制应用程序是一个非常有用的界面:

  • 在ASP.NET Core中探索IStartupFilter(IStartupFilter的简介)
  • 通过Middleware Analysis软件包了解您的中间件管道(剧透警报-使用IStartupFilter)
  • 向ASP.NET Core中的强类型配置对象添加验证(再次使用IStartupFilter)

如果您不熟悉过滤器,建议阅读我的介绍性文章,但是我将在此处提供一个简短的摘要。

IStartupFilter在配置中间件管道的过程中执行(通常在中完成Startup.Configure())。它们允许您通过插入额外的中间件,对它进行分叉或执行许多其他操作来定制由应用程序实际创建的中间件管道。例如,以下AutoRequestServicesStartupFilter 所示在管道的开头插入了一个新的中间件:

1
2
3
4
5
6
7
8
9
10
11
public class AutoRequestServicesStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<RequestServicesContainerMiddleware>();
next(builder);
};
}
}

这很有用,但是与在应用程序启动时运行一次性任务有什么关系?

该IStartupFilter所提供的主要功能是,在设置了配置并配置了依赖项注入容器之后,但在应用程序准备启动之前,可以钩住早期的应用程序启动过程。这意味着您可以将依赖注入与IStartupFilters一起使用,因此几乎可以运行任何代码。而DataProtectionStartupFilter则用于初始化数据保护系统。我使用了类似的IStartupFilter方法来提供对强类型配置的热验证。

另一个非常有用的功能是,它允许您通过在DI容器中注册服务来添加要执行的任务。这意味着作为源代码作者,您可以注册要在应用程序启动时运行的任务,而无需应用程序作者显式调用它。

那么为什么我们不能只IStartupFilter在启动时用来运行异步任务呢?

问题在于IStartupFilter基本上是同步的。该Configure()方法(您可以在上面的代码中看到)没有返回a Task,因此尝试通过async进行同步不是一个好主意。我稍后再讨论,但是现在绕道而行。

为什么不使用健康检查?img

ASP.NET Core 2.2为ASP.NET Core应用程序引入了运行状况检查功能,该功能使您可以查询通过HTTP端点公开的应用程序的“运行状况”。部署后,编排引擎(如Kubernetes)或反向代理(如HAProxy和NGINX)可以查询此终结点,以检查您的应用是否准备好开始接收请求。

您可以使用运行状况检查功能来确保您的应用程序在所有必需的一次性任务完成之前不会开始服务请求(即从运行状况检查端点返回“运行状况”状态)。但是,这有一些缺点:

  • 在WebHost已执行的一次性任务前,探针会启动。尽管他们不会收到“实际”请求(仅运行状况检查请求),但这仍然可能是一个问题。
  • 它引入了额外的复杂性。除了添加代码以运行一个任务之外,您还需要添加运行状况检查以测试任务是否完成,并同步任务的状态。
  • 应用程序的启动仍会延迟到所有任务都完成为止,因此不太可能减少启动时间。
  • 如果任务失败,则该应用将继续以“死”状态运行,在此状态下,运行状况检查将永远不会通过。这可能是可以接受的,但就我个人而言,我更喜欢一个应用程序立即失败。
  • 运行状况检查方面仍未定义如何实际运行任务,仅定义任务是否成功完成。您仍然需要确定一种在启动时运行任务的机制。

对我来说,健康检查似乎不适合一次性任务的情况。对于我描述的某些示例,它们可能很有用,但我认为它们并不适合所有情况。我真的很想能够在应用启动时运行一次性任务,然后再运行WebHost。

运行异步任务img

我花了很长时间讨论所有无法实现目标的方法,以及一些解决方案!在本节中,我将介绍一些运行异步任务(即返回a Task并需要await-ing的任务)的可能性。有些比其他的要好,有些则应避免,但我想介绍各种选择。

为了具体讨论,我将考虑数据库迁移示例。在EF Core中,您可以在运行时通过调用迁移数据库myDbContext.Database.MigrateAsync(),其中myDbContext是应用程序的实例DbContext。

该方法还有一个同步版本Database.Migrate(),但只是假装目前没有!

1.使用 IStartupFilterimg

前面已经介绍了如何在应用程序启动时IStartupFilter用于运行同步任务。不幸的是,运行异步任务的唯一方法是使用“异步同步”方法,我们称之为GetAwaiter().GetResult():

警告:此代码使用了不合适的async做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MigratorStartupFilter: IStartupFilter
{
// We need to inject the IServiceProvider so we can create
// the scoped service, MyDbContext
private readonly IServiceProvider _serviceProvider;
public MigratorStartupFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
// Create a new scope to retrieve scoped services
using(var scope = _seviceProvider.CreateScope())
{
// Get the DbContext instance
var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

//Do the migration by blocking the async call
myDbContext.Database.MigrateAsync()
.GetAwaiter() // Yuk!
.GetResult(); // Yuk!
}

// don't modify the middleware pipeline
return next;
}
}

这很可能不会引起任何问题-此代码仅在应用程序启动时运行,然后再处理请求,因此似乎不太可能出现死锁。但是坦率地说,我不能肯定地说,我会尽可能避免这样的代码。

大卫·福勒(David Fowler)整理了一些正确的异步编程指南。我强烈建议您阅读!

2.使用IApplicationLifetime事件img

我之前没有讨论过太多,但是当您的应用程序通过IApplicationLifetime界面启动和关闭时,您可以收到通知。我这里不做详细介绍,因为它对于我们的目的有一些问题:

  • IApplicationLifetime使用CancellationTokens来注册回调,这意味着您只能同步执行回调。从本质上讲,这意味着您无论采取什么操作,都必须坚持使用异步模式进行同步。
  • 该ApplicationStarted事件仅在开始了WebHost后运行,所以任务应用程序启动时接受请求后运行。

鉴于其没有解决IStartupFilter的异步同步问题,也没有阻止应用启动,因此我们将IApplicationLifetime继续进行下一个可能性。

3. IHostedService用于运行异步任务img

IHostedService允许ASP.NET Core应用在应用生命周期内在后台执行长时间运行的任务。它们有许多不同的用途-您可以使用它们在计时器上运行定期任务,处理其他消息传递范例,例如RabbitMQ消息或许多其他事情。在ASP.NET Core 3.0中,甚至ASP.NET Web主机也可能建立在之上IHostedService。

在IHostedService本质上是异步的,同时具有StartAsync和StopAsync功能。这对我们来说很棒,因为这意味着不再需要通过异步进行同步!将数据库迁移器实现为托管服务可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MigratorHostedService: IHostedService
{
// We need to inject the IServiceProvider so we can create
// the scoped service, MyDbContext
private readonly IServiceProvider _serviceProvider;
public MigratorStartupFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
// Create a new scope to retrieve scoped services
using(var scope = _seviceProvider.CreateScope())
{
// Get the DbContext instance
var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

//Do the migration asynchronously
await myDbContext.Database.MigrateAsync();
}
}

public Task StopAsync(CancellationToken cancellationToken)
{
// noop
return Task.CompletedTask;
}
}

不幸的是,IHostedService这不是我们所希望的灵丹妙药。它允许我们编写真正的异步代码,但是它有两个问题:

  • IHostedServices 的典型实现期望StartAsync函数返回相对较快。对于后台服务,预计您将异步启动该服务,但是大部分工作将在该启动代码之外进行(请参阅docs中的示例)。因此,“内联”迁移数据库不是问题,但是它将阻止其他IHostedServices的启动,这可能会或可能不会发生。
  • IHostedService.StartAsync()被称为后的WebHost启动,所以你不能用这种方式来运行的任务之前,您的应用程序启动。

最大的问题是第二个问题-应用程序将在IHostedService运行数据库迁移之前开始接受请求,这不是我们想要的。回到绘图板。

4.在Program.cs中手动运行任务img

到目前为止,没有显示解决方案可以提供完整的解决方案。他们要么需要在异步编程上使用同步(虽然在应用程序启动的上下文中可以这样做,但并不鼓励这样做),或者不阻止应用程序启动。到目前为止,有一个简单的解决方案我已经忽略了,那就是停止尝试使用框架机制,而是自己完成这项工作。

ASP.NET Core模板中使用的默认Program.cs会IWebHost在Main函数中生成并运行一条语句:

1
2
3
4
5
6
7
8
9
10
11
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}

但是,在Build()创建之后WebHost,但是在您调用之前,没有什么阻止您运行代码Run()。再加上允许您的Main函数异步的C#7.1功能,我们有一个合理的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Program
{
public static async Task Main(string[] args)
{
IWebHost webHost = CreateWebHostBuilder(args).Build();

// Create a new scope
using (var scope = webHost.Services.CreateScope())
{
// Get the DbContext instance
var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();

//Do the migration asynchronously
await myDbContext.Database.MigrateAsync();
}

// Run the WebHost, and start accepting requests
// There's an async overload, so we may as well use it
await webHost.RunAsync();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}

该解决方案具有许多优点:

  • 我们现在正在执行真正的异步,不需要通过异步进行同步
  • 我们可以异步执行任务
  • 在执行任务后,该应用才接受请求
  • 此时已构建了DI容器,因此我们可以使用它来创建服务。

不幸的是,这并非所有的好消息。我们仍然缺少一些东西:

  • 即使已经构建了DI容器,也没有中间件管道。这不会发生,直到你打电话Run()或RunAsync()上IWebHost。到那时,构建了中间件管道,IStartupFilter执行了,然后启动了应用程序。如果您的异步任务需要在以下任何步骤中进行配置,那么您就不走运了
  • 通过将服务添加到DI容器,我们失去了自动运行任务的能力。我们必须记住要手动运行任务。

如果这些警告不是问题,那么我认为此最终选择将为解决该问题提供最佳解决方案。在我的下一篇文章中,我将展示一些方法,我们可以采用这个基本示例并在其基础上进行构建,以使某些操作变得更容易使用。

摘要img

在本文中,我讨论了在应用启动时异步运行任务的需求。我描述了这样做的一些挑战。对于同步任务,IStartupFilter它提供了一个有用的钩子,可以连接到ASP.NET Core应用程序的启动过程,但是运行异步任务需要通过异步编程进行同步,这通常是一个坏主意。我描述了许多用于运行异步任务的可能选项,我发现最好的方法是在Program.cs中 “手动”运行任务,并在构建IWebHost和运行它之间进行。在下一篇文章中,我将提供一些代码来简化此模式的使用。

LINQ:最终统治了所有的语言!

Posted on 2021-02-17 | Edited on 2021-04-25 | In 技术

LINQ:最终统治了所有的语言!

让我们看看LINQ如何彻底改变了.NET中访问数据的方式

NETQ与其他技术堆栈的不同之处之一绝对是LINQ,它是Language Integrated Query的首字母缩写。实际上,它是随.NET Framework 3.5和Visual Studio 2008引入的,它是第一个独立于体系结构并集成在C#和Visual Basic语言中的框架。

借助LINQ,我们可以使用独立于各种源的单个编程模型来查询和操作数据。为了更好地理解它是什么,我们必须跳过去。

在C#的第一个版本中,我们必须使用for或foreach循环来遍历一个集合,正如我们所知,该集合实现IEnumerable接口,例如,在其中找到一个特定的对象。以下代码返回公司年龄在19至36岁(20至35岁)之间的所有客户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Customer
{
public int CustomerID { get; set; }
public String CustomerName { get; set; }
public int Age { get; set; }
}

class Program
{
static void Main(string[] args)
{
Customer[] customerArray = {
new Customer() { CustomerID = 1, CustomerName = "Joy", Age = 22 },
new Customer() { CustomerID = 2, CustomerName = "Bob", Age = 45 },
new Customer() { CustomerID = 3, CustomerName = "Curt", Age = 25 },
};

Customer[] customers = new Customer[10];

int i = 0;

foreach (Customer cst in customerArray)
{
if (cst.Age > 19 && cst.Age < 36)
{
customers[i] = cst;
i++;
}
}
}
}

有什么不同的方法?让我们尝试从“ 委托”的概念开始逐步进行开发。甲代表是表示与相同的参数和返回的类型的方法的引用类型。它“委托”它旨在执行代码的方法,我们可以用这种方式声明它:

1
public delegate bool Operations(int number);

此委托可以指向所有接受输入整数并返回布尔值的方法。例如,假设在CustomerOperations类中有一个方法:

1
2
3
4
public bool CustomerAgeRangeCheck(int number)
{
return number > 19 && number < 36;
}

我们可以注册一个或多个将在执行委托时执行的方法:

1
Operations op = new Operations(CustomerOperations.CustomerAgeRangeCheck);

或者简单地:

1
Operations op = CustomerOperations.CustomerAgeRangeCheck;

因此,我们可以使用委托,在这种情况下,它将返回true:

1
op(22);

委托用于将方法作为参数传递给其他方法:事件处理程序和回调是通过委托调用的方法的示例。

C#2.0引入了匿名委托,您现在可以使用匿名方法来声明和初始化委托。例如,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
delegate bool CustomerFilters(Customer customer);

class CustomerOperations
{
public static Customer[] FindWhere(Customer[] customers, CustomerFilters customerFiltersDelegate)
{
int i = 0;
Customer[] result = new Customer[6];
foreach (Customer customer in customers)
if (customerFiltersDelegate(customer))
{
result[i] = customer;
i++;
}

return result;
}
}

class Program
{
static void Main(string[] args)
{
Customer[] customers = {
new Customer() { CustomerID = 1, CustomerName = "Joy", Age = 22 },
new Customer() { CustomerID = 2, CustomerName = "Bob", Age = 45 },
new Customer() { CustomerID = 3, CustomerName = "Curt", Age = 25 },
};

Customer[] filteredCustomersAge = CustomerOperations.FindWhere(customers, delegate (Customer customer) //Using anonimous delegate
{
return customer.Age > 19 && customer.Age < 36;
});
}
}

使用C#2.0,我们的优势是可以使用匿名委托在不同条件下进行搜索,而无需使用for或foreach循环。例如,我们可以使用上一个示例中相同的委托函数来查找“ CustomerID”为3或名称为“ Bob”的客户:

1
2
3
4
5
6
7
8
9
Customer[] filteredCustomersId = CustomerOperations.FindWhere(customers, delegate (Customer customer)  
{
return customer.CustomerID == 3;
});

Customer[] filteredCustomersName = CustomerOperations.FindWhere(customers, delegate (Customer customer)
{
return customer.CustomerName == "Bob";
});

随着C#的发展,从3.x版本开始,Microsoft团队引入了新功能,使代码更加紧凑和易读。这些直接支持LINQ来查询不同类型的数据源并获得产生单个指令的元素。

这些功能是:

-在VAR结构,一个隐式类型的局部变量。它是强类型化的,因为已经声明了类型本身,但是由编译器根据分配给它的值使用类型推断来确定类型。以下两个语句在功能上等效:

1
2
var customerAge = 30; // Implicitly typed.
int customerAge = 30; // Explicitly typed.

-使用 对象初始化程序, 您可以在对象创建期间将值分配给对象的全部或某些属性,而无需在分配指令行之后调用构造函数。

1
Customer customer = new Customer { Age = 30, CustomerName = "Adolfo" };

与以下代码不同,在前一种情况下,所有内容都被视为单个操作。

1
2
3
Customer customer = new Customer();
customer.Age = 30;
customer.CustomerName = "Adolfo";

- 匿名类型,由编译器构建的只读类型,只有编译器知道它。但是,如果程序集中的两个或多个匿名对象初始化程序具有相同顺序的属性序列,并且具有相同的名称和类型,则编译器会将这些对象视为相同类型的实例。匿名类型是将查询结果中的一组属性临时分组的好方法,而不必定义单独的命名类型。

1
var customer = new { YearsOfFidelity = 10, Name = "Francesco"};

-扩展方法,使您可以将方法“添加”到现有类型,而无需创建新的派生类型,重新编译或修改原始类型。扩展方法是静态方法,但由于引入了语法糖,因此被称为,因为它们是扩展类型上的实例方法。

1
2
3
4
5
6
7
8
public static class StringExtensionMethods
{
public static string ReverseString(this string input)
{
if (string.IsNullOrEmpty(input)) return "";
return new string(input.ToCharArray().Reverse().ToArray());
}
}

扩展方法必须在静态类中定义。第一个参数表示要扩展的类型,并且必须以关键字this开头,其他参数则不需要它。

1
Console.WriteLine("Hello".ReverseString());   //olleH

请注意,在方法调用中不得指定第一个参数,该参数以this修饰符开头。

- Lambda表达式,可作为可变的或作为在一方法调用中的参数被传递匿名函数。

1
customer => customer.Age > 19 && customer.Age < 36;

=>运算符称为lambda运算符,而customer是函数的输入参数。lambda运算符右侧的部分代表函数的主体及其返回的值,在这种情况下为布尔值。

在LINQ的引入中,我们终于有了C#3.5版本。

简而言之,我们可以说LINQ是IEnumerable 和IQueryable 接口的扩展方法库,它使我们能够执行各种操作,如过滤,进行投影,聚合和排序。

我们有几种可用的LINQ实现:

  • LINQ到对象(内存中对象集合)
  • LINQ到实体(实体框架)
  • LINQ to SQL(SQL数据库)
  • LINQ to XML(XML文档)
  • LINQ到数据集(ADO.Net数据集)
  • 通过实现IQueryable接口(其他数据源)

在前面的示例中,数组用作数据源,因此隐式支持通用接口IEnumerable <T <。支持IEnumerable 或其派生接口的类型(例如通用IQueryable 接口)称为可查询类型,使我们可以直接执行LINQ查询。如果数据源尚未以可查询类型存储在内存中,则LINQ提供程序必须将其表示为可查询类型。

正如我们所说,LINQ查询主要基于.NET Framework 2.0版中引入的通用类型。这意味着,例如,如果尝试将Customer对象添加到List 对象,则在编译时将生成错误。使用通用集合很容易,因为不需要在运行时强制转换类型。
如果愿意,可以使用前面提到的var关键字来避免通用语法,在下面的示例中,该关键字要求编译器通过检查from子句中指定的数据源来推断查询变量的类型。
因此,让我们看看如何能达到同样的效果,在前面的代码中我们获得了使用匿名委托,使用LINQ到对象查询,该变种构造和lambda表达式:

1
var filteredCustomersAge = customers.Where(c => c.Age > 19 && c.Age < 36);

这种语法称为方法语法。
在下一个示例中,我们将使用查询语法(Query Syntax),该语法是为那些已经了解SQL语言并且因此会喜欢这种方法的人引入的:

1
2
3
4
var filteredCustomersAge =
from customer in customers
where customer.Age > 19 && customer.Age < 36
select customer;

查询语法和方法语法在语义上是相同的,许多人发现查询的语法更简单,更易于阅读。
在查询语法中,LINQ查询运算符在编译时转换为对相关LINQ 扩展方法的调用。

在下一篇文章中,我们将继续讨论LINQ!
我们将讨论IQueryable 接口,其相关的LINQ扩展方法以及与IEnumerable 接口的区别。
与远程数据库一样,我们还将看到LINQ与内存外集合的数据源一起使用。

浅议C#客户端和服务端通信的几种方法:Rest和GRPC

Posted on 2020-12-20 | Edited on 2021-04-25 | In 技术

浅议C#客户端和服务端通信的几种方法:Rest和GRPC

在C#客户端和C#服务器之间进行通信的方法有很多。一些功能强大,而其他功能则不是很多。有些非常快,有些则不是。知道不同的选择很重要,这样您才能决定最适合自己的选择。

本文将介绍当今最流行的技术,以及为何如此广泛地使用它们。我们将讨论REST,gRPC及其两者之间的所有内容。

最佳方案

让我们考虑一下我们希望如何在最佳环境中使客户端与服务器之间的通信看起来像。我在想像这样的东西:

1
2
3
4
5
6
// on client side
public void Foo()
{
var server = new MyServer(new Uri("https://www.myserver.com/");)
int sum = server.Calculator.SumNumbers(12,13);
}
1
2
3
4
5
6
7
8

// on server side
class CalculatorController : Controller{
public int SumNumbers(int a, int b)
{
return a + b;
}
}

我当然想要完整的Intellisense。当我单击server并. 希望Visual Studio显示所有控制器时。当我单击CalculatorController和时.,我想查看所有操作。我还想要一流的性能,很少的网络负载和双向通信。而且我想要一个能够完美处理版本控制的强大系统,这样我就可以毫不费力地部署新的客户端版本和新的服务器版本。

要求太多吗?

请注意,我在这里谈论的是无状态API。这等效于C#项目,其中只有两种类型的类:

  • 静态类,只有静态方法。
  • POCO类仅具有类型为基本类型或其他POCO类的字段和属性。

在API中使用状态会带来复杂性,而这正是万恶之源。因此,为了本文的方便,让我们保持美好和无状态。

传统REST

REST API出现在2000年代初期,席卷了整个互联网。到目前为止,它是创建Web服务的最流行的方法。

REST为客户端到服务器的请求定义了一组固定的操作GET,POST,PUT和DELETE。每个请求都将通过包含有效负载(通常为JSON)的响应来回答。请求包含在查询本身中的参数,或者在它是POST请求时包含为有效负载(通常为JSON)的参数。

有一个称为RESTful API的标准,它定义了以下规则(您实际上不必使用它):

  • GET用于检索资源
  • PUT用于更改资源状态
  • POST用于创建资源
  • DELETE用于删除资源

如果您到目前为止还不熟悉REST,则上面的解释可能不会减少它,因此这里有一个示例。在.NET中,内置了对REST的支持。实际上,默认情况下,ASP.NET Web API被构建为REST Web服务。这是典型的客户端和ASP.NET服务器的外观:

在服务器中:

1
2
3
4
5
6
7
8
9
10
[Route("People")]
public class PeopleController : Controller
{
[HttpGet]
public Person GetPersonById(int id)
{
Person person = _db.GetPerson(id);
return person;//Automatically serialized to JSON
}
}

在客户中:

1
2
3
4
var client = new HttpClient();
string resultJson =
await client.GetStringAsync("https://www.myserver.com/People/GetPersonById?id=123");
Person person = JsonConvert.DeserializeObject<Person>(resultJson);

REST非常方便,但是并没有达到最佳方案。因此,让我们看看是否可以做得更好。

ReFit

ReFit不能替代REST。相反,它建立在REST之上,并允许我们像调用简单方法一样调用服务器端点。这是通过在客户端和服务器之间共享接口来实现的。在服务器端,您的控制器将实现一个接口:

1
2
3
4
5
public interface IMyEmployeeApi
{
[Get("/employee/{id}")]
Task<Employee> GetEmployee(string id);
}

然后,在客户端,您需要包括相同的接口并使用以下代码:

1
2
var api = RestService.For<IMyEmployeeApi>("https://www.myserver.com");
var employee = await api.GetEmployee("abc");

就这么简单。除了几个NuGet软件包外,无需运行困难的自动化程序或使用任何第三方工具。

这更接近最佳方案。现在,我们有了IntelliSense,并且客户端和服务器之间的合同很牢固。但是还有另一种选择,在某些方面甚至更好。

昂首阔步

像ReFit一样,Swagger也建立在REST之上。OpenAPI或Swagger是REST API的规范。它描述了具有简单JSON文件的REST Web服务。这些文件是Web服务的API架构。它们包括:

  • API中的所有路径(URL)。
  • 每个路径的预期操作(GET,POST等)。每个路径可以处理不同的操作。例如,单个路径https://mystore.com/Product可能接受添加产品的POST操作和返回产品的GET操作。
  • 每个路径和操作的预期参数。
  • 每个路径的预期响应。
  • 每个参数和响应对象的类型。

该JSON文件实质上是客户端和服务器之间的合同。这是一个描述一个称为Swagger Petstore的Web服务的swagger文件的示例(为清楚起见,我删除了一些部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{ 
"swagger":"2.0",
"info":{
"version":"1.0.0",
"title":"Swagger Petstore",
"description":"A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
},
"host":"petstore.swagger.io",
"basePath":"/api",
"schemes":[
"http"
],
"consumes":[
"application/json"
],
"produces":[
"application/json"
],
"paths":{
"/pets":{
"get":{
"description":"Returns all pets from the system that the user has access to",
"operationId":"findPets",
"produces":[
"application/json",
"application/xml",
],
"parameters":[
{
"name":"tags",
"in":"query",
"description":"tags to filter by",
"required":false,
"type":"array",
"items":{
"type":"string"
},
"collectionFormat":"csv"
},
{
"name":"limit",
"in":"query",
"description":"maximum number of results to return",
"required":false,
"type":"integer",
"format":"int32"
}
],
"responses":{
"200":{
"description":"pet response",
"schema":{
"type":"array",
"items":{
"$ref":"#/definitions/Pet"
}
}
},
...

让我们考虑一下这个结果。使用上面的JSON文件,您可以潜在地创建具有完整IntelliSense的C#客户端。毕竟,您知道所有路径,操作,它们期望的参数,什么参数类型,什么是响应等等。

有几种工具可以做到这一点。对于服务器端,可以使用Swashbuckle.AspNetCore将Swagger添加到ASP.NET中并生成所述JSON文件。对于客户端,您可以使用swagger-codegen和AutoRest来使用这些JSON文件并生成客户端。让我们看一个如何做到这一点的例子:

将Swagger添加到ASP.NET服务器

首先添加NuGet包Swashbuckle.AspNetCore。在中ConfigureServices,注册Swagger生成器:

1
2
services.AddSwaggerGen(options => 
options.SwaggerDoc("v1", new OpenApiInfo {Title = "My Web API", Version = "v1"}));

在添加Configure方法中Startup.cs:

1
app.UseSwagger();

最后,控制器内部的动作应使用[HttpXXX]和[FromXXX]属性修饰:

1
2
3
4
5
6
7
8
9
10
11
[HttpPost]
public async Task AddEmployee([FromBody]Employee employee)
{
//...
}

[HttpGet]
public async Task<Employee> Employee([FromQuery]string id)
{
//...
}

就像服务器端一样简单。运行项目时,swagger.json将生成一个文件,可用于生成客户端。

使用AutoRest从Swagger生成客户端

要开始使用AutoRest,与安装NPM:npm install -g autorest。安装后,您将需要使用AutoRest的命令行界面从该swagger.json文件生成C#客户端。这是一个例子:

1
autorest --input-file="./swagger.json" --output-folder="GeneratedClient" --namespace="MyClient" --override-client-name="MyClient" --csharp

这将产生一个GeneratedClient包含生成的C#文件的文件夹。请注意,名称空间和客户端名称被覆盖。从这里,将此文件夹添加到Visual Studio中的客户端项目。

Visual Studio中的AutoRest

您需要安装Microsoft.Rest.ClientRuntimeNuGet软件包,因为生成的代码取决于该软件包。安装后,您可以像使用常规C#类一样使用API:

1
2
var client = new MyClient();
Employee employee = client.Employee(id: "abc");

您可以在AutoRest的文档中阅读一些细微之处。而且您需要使该过程自动化,因此我建议阅读Patrik Svensson的教程,以获得一些好的建议以及Peter Jausovec的这篇文章。

Swagger的问题是JSON文件是在运行时创建的,因此这使得在CI / CD流程中实现自动化有点困难。

传统REST vs Swagger vs ReFit

进行选择时,请注意以下几点。

  • 如果您有一个非常简单的私有REST API,则也许不必理会客户端生成和共享接口。小任务并不能证明付出额外的努力是合理的。
  • Swagger支持多种语言,而ReFit仅支持.NET。Swagger还是许多工具,测试,自动化和UI工具的基础。如果您要创建一个大型的公共API,它将可能是最佳选择。
  • Swagger比ReFit复杂得多。使用ReFit,只需在服务器和客户端项目中添加一个接口即可。另一方面,使用ReFit,您必须为每个控制器创建新的接口,而Swagger会自动进行处理。

但是在决定任何事情之前,请检查与REST无关的第四个选项。

gRPC

gRPC(gRPC远程过程调用)是Google开发的开源远程过程调用系统。它有点像REST,它提供了一种将请求从客户端发送到服务器的方式。但这在许多方面都不同,这是相同点和不同点:

  • 像REST一样,gRPC与语言无关。有适用于所有流行语言的工具,包括C#。
  • gRPC是契约的基础,并使用.proto文件来定义契约。这有点类似于Swaggerswagger.json和ReFit的共享界面。可以从那些文件中生成任何编程语言的客户端。
  • gRPC使用协议缓冲区(Protobuf)二进制序列化。这与REST(通常序列化为JSON或XML)不同。二进制序列化较小,因此更快。
  • gRPC用于使用HTTP / 2协议创建持久连接。该协议更简单,更紧凑。REST使用HTTP 1.x协议(通常为HTTP 1.1)。
  • HTTP 1.1要求每个请求都进行TCP握手,而HTTP / 2则保持连接打开。
  • HTTP / 2连接使用多路复用流。这意味着单个TCP连接可以支持许多流。这些流可以并行执行,而不必像HTTP 1.1中那样互相等待。
  • gRPC允许双向流。

有两种使用gRPC的方法。对于.NET Core 3.0,有一个完全托管的库,称为.NET的gRPC。对于其中的任何内容,您都可以使用gRPC C#,它是使用本机代码构建的。这并不意味着适用于.NET的gRPC可以替代gRPC C#。让我们来看一个用于.NET的更新gRPC的示例。

.NET的gRPC的服务器端

这不是教程,而是更多有关预期内容的一般性想法。这是示例控制器在gRPC中的外观:

1
2
3
4
5
6
7
8
9
10
11
12
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(HelloRequest request,
ServerCallContext context)
{
_logger.LogInformation("Saying hello to {Name}", request.Name);
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}

您需要添加以下的Configure在Startup.cs:

1
2
3
4
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
});

API在.proto文件中描述,该文件是项目的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

syntax = "proto3";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

此.proto文件添加到.csproj:

1
2
3
<ItemGroup>
<Protobuf Include="Protos\greet.proto" />
</ItemGroup>

.NET的gRPC客户端

客户端是从.proto文件生成的。代码本身非常简单:

1
2
3
4
5
6
7
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

var response = await client.SayHello(
new HelloRequest { Name = "World" });

Console.WriteLine(response.Message);

gRPC与REST

gRPC听起来不错。它在框架下更快,更简单。那么,我们都应该从REST变为gRPC吗?答案是,这取决于你的应用场景。

以下是一些注意事项:

从我的印象来看,使用gRPC和ASP.NET仍然不是很好。借助对REST的成熟支持,您会变得更好。就基于契约的通信而言,这很不错,除了在REST中有我们已经讨论过的类似替代方案:Swagger和ReFit。

最大的优势是性能。根据这些基准,在大多数情况下,gRPC更快。特别是对于大型有效载荷,Protobuf序列化确实有所作为。这意味着对于高负载服务器而言,这是一个巨大的优势。

在大型ASP.NET应用程序中从REST过渡到gRPC将非常困难。但是,如果您具有基于微服务的体系结构,那么逐步完成此过渡就变得容易得多。

其他沟通方式

还有其他一些我完全没有提及的通信方式,但是值得一提的是:

  • GraphQL是Facebook开发的API的查询语言。它允许客户端从服务器确切地要求它需要的数据。这样,您可以在服务器上仅创建一个端点,该端点将非常灵活,并且仅返回客户端所需的数据。近年来,GraphQL变得非常流行。
  • SignalR是一项允许服务器与客户端之间进行实时双向通信的技术。SignalR不仅允许客户端始终向服务器发送请求,还允许服务器向客户端发送推送通知。这样可以查看Web应用程序中的实时更新。SignalR在ASP.NET中非常流行。
  • TcpClient和TcpListener(在中System.Net.Sockets)提供基于TCP的低级连接。基本上,您将建立连接并传输字节数组。对于大型应用程序而言,它不是理想的选择,在大型应用程序中,您可以使用ASP.NET的控制器和操作在大型API中进行订购。
  • UdpClient提供了一种通过UDP协议进行通信的方法。TCP建立连接,然后发送数据,而UDP仅发送数据。TCP确保数据中没有错误,而UDP没有。UDP可以更有效地快速传输数据,您不必担心它是否可靠且没有错误。一些示例是:视频流,实时广播和IP语音(VoIP)。
  • WCF是一种较旧的技术,主要在进程之间使用基于SOAP的通信。这是一个庞大的框架,我要说的是它已不再受REST和JSON负载的欢迎。
12…9

溪源

81 posts
4 categories
4 tags
博客园 CSDN
© 2021 溪源
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.1.2
#
ICP备案号:湘ICP备19001531号-2