即时设计

人们从为Java编写的旧书中学习体系结构。 这些书不错,但是它们使用当时的仪器为当时的问题提供了解决方案。 时间已经改变,C#与light Scala相比,与Java更相似,而且几乎没有新的好书。

在本文中,我们将研究好的代码和坏的代码的标准,如何以及如何衡量。 我们将看到典型任务和方法的概述,我们将分析其优缺点。 最后,将提供设计Web应用程序的建议和最佳实践。

本文是我在DotNext 2018莫斯科会议上的报告的笔录。 除了文字之外,还有视频和切面下方的幻灯片链接。



网站上的 幻灯片报告页面
关于我的简介:我来自喀山,我在高科技集团工作。 我们正在开发商务软件。 最近,我在喀山联邦大学教授一门名为“企业软件开发”的课程。 我仍然不时在Habr上写有关工程实践和企业软件开发的文章。

您可能已经猜到了,今天我将讨论企业软件的开发,即如何构建现代Web应用程序:

  • 标准
  • 建筑思想发展的简要历史(过去是什么,变成了什么,出现了什么问题);
  • 经典粉扑体系结构缺陷的概述
  • 决定
  • 逐步分析实施情况而无需深入细节
  • 结果。

标准


我们制定标准。 当我说“我的功夫比你的功夫更强大”时,我真的不喜欢它。 一家公司原则上有一个称为金钱的特定标准。 每个人都知道时间就是金钱,所以这两个部分通常是最重要的。



因此,标准。 原则上,企业通常会问我们“每单位时间尽可能多的功能”,但有一个警告-这些功能应该起作用。 可能会中断的第一步是代码审查。 也就是说,程序员似乎说:“我会在三个小时内完成。” 三个小时过去了,审核进入了代码,团队负责人说:“哦,不,请重做。” 还有另外三个-代码审查已经通过了多少次迭代,因此您需要乘以三个小时。

接下来的一点是验收测试阶段的收益。 一样 如果该功能无法正常运行,则说明该功能无法正常运行,这三个小时可以持续一周,两个小时-照常进行。 最后一个标准是回归和错误的数量,尽管经过了测试和认可,但回归和错误仍通过了生产。 这也很糟糕。 这个标准存在一个问题。 这很难跟踪,因为我们很难将某些东西推入存储库的事实与两周后某事物损坏的事实之间的联系。 但是,仍然有可能。

建筑发展


曾几何时,当程序员刚刚开始编写程序时,仍然没有架构,每个人都能做自己喜欢的一切。



因此,我们得到了这样的建筑风格。 这在这里被称为“面条代码”,在国外被称为“意大利面条代码”。 一切都与一切联系在一起:我们在A点改变了某些东西-在B点打破了它,完全不可能理解什么与什么联系了。 自然,程序员很快意识到这是行不通的,必须完成一些结构,并决定某些层可以为我们提供帮助。 现在,如果您认为肉末是代码,而千层面是这样的层,那么这是分层体系结构的一个示例。 碎肉仍然切碎,但是现在第1层的碎肉不能直接与第2层的碎肉交谈。我们给出了某种形式的代码:即使在图片中,您也可以看到攀登更加框架化。



每个人都可能熟悉经典的分层体系结构 :有一个UI,一个业务逻辑和一个数据访问层。 仍然有各种各样的服务,外墙和层,以退出公司的建筑师的名字命名,其中可以有无限数量。



下一阶段是所谓的洋葱架构 。 似乎存在巨大差异:在此之前有一个小正方形,在这里有多个圆圈。 似乎完全不同。



不完全是 整个差异是在那时某个地方制定了SOLID的原理,结果证明在经典洋葱中存在依赖反转的问题,因为抽象域代码出于某种原因取决于实现方式和数据访问方式,因此我们决定部署数据访问方式,并且数据访问取决于域。



在这里,我练习绘画并绘制洋葱的结构,但不是经典的“圆环”。 我在多边形和圆圈之间有东西。 我这样做只是为了表明,如果您遇到“洋葱”,“六角”或“端口和适配器”这两个词,它们都是一样的。 关键是,域位于中心,它包装在服务中,您可以根据需要将其作为域或应用程序服务。 外部用户界面,测试和DAL迁移到的基础结构的形式-它们通过此服务层与域进行通信。

一个简单的例子。 电邮更新


让我们看看一个简单的用例在这种范例中的样子-更新用户的电子邮件地址。



我们需要发送请求,验证,更新数据库中的值,向新电子邮件发送通知:“一切正常,您更改了电子邮件,我们知道一切都很好”,然后回复“ 200”浏览器-一切正常。



该代码可能看起来像这样。 这里我们有标准的ASP.NET MVC验证,有ORM可以读取和更新数据,并且有某种发送通知的电子邮件发送者。 看起来一切都很好,对吧? 一个警告-在理想的世界中。

在现实世界中,情况略有不同。 关键是添加授权,错误检查,格式化,日志记录和性能分析。 所有这些都与我们的用例无关,但应该都是如此。 而且那一小段代码变得庞大而令人恐惧:具有大量的嵌套,大量的代码,而且难以阅读,而且最重要的是,基础结构代码比域代码更多。



“服务在哪里?” -你说。 我将所有逻辑都写给了控制器。 当然,这是一个问题,现在我将添加服务,一切都会好起来。



我们增加了服务,它的确变得更好,因为我们得到了一条漂亮的小线,而不是用一块大的鞋垫。

变得更好了吗? 变成了! 现在,我们可以在不同的控制器中重用此方法。 结果很明显。 让我们看一下该方法的实现。



但是这里的一切都不是很好。 这段代码仍然在这里。 我们只是将同一件事转移到服务中。 我们决定不解决问题,而只是掩饰它并将其转移到另一个地方。 仅此而已。



除此之外,还会出现其他一些问题。 我们应该在控制器中还是在这里进行验证? 好吧,有点像在控制器中。 并且,如果您需要进入数据库并看到有这样的ID或没有其他用户收到这样的电子邮件,该怎么办? 嗯,那么服务中。 但是这里的错误处理? 该错误处理可能在此处,并且该错误处理将响应控制器中的浏览器。 还有SaveChanges方法,它在服务中还是您需要将其传输到控制器? 之所以如此,是因为如果调用了一项服务,则在服务中进行调用更合乎逻辑,并且如果控制器中需要调用三种服务方法,则需要在这些服务之外进行调用,以便事务为一。 这些反映表明,也许这些层不能解决任何问题。



这个想法发生在一个以上的人身上。 如果您使用Google搜寻,这些可敬的丈夫中至少有三位写的是同一件事。 从上到下:Stephen .NET Junkie(不幸的是,我不知道他的姓氏,因为她没有出现在Internet上的任何地方),是Simple Injector IoC容器的作者。 Next Jimmy Bogard是AutoMapper的作者。 下面是F#的作者Scott Vlashin,他表示乐趣和收益



所有这些人都在谈论同一件事,并建议构建应用程序不是基于层,而是基于用例,即业务要求我们的那些需求。 因此,可以使用IHandler接口确定C#中的用例。 它具有输入值,具有输出值,并且有一种方法本身可以实际执行此用例。



在此方法内部,可以是域模型或某些非规范化的模型以供读取,如果需要查找内容,则可以使用Dapper或Elastic Search,或者您可以使用Legacy -具有存储过程的系统-没问题,以及网络请求-总体而言,在那里您可能需要的任何东西。 但是,如果没有图层,该怎么办?



首先,让我们摆脱UserService。 我们删除该方法并创建一个类。 我们将其删除,然后再次将其删除。 然后删除该类。



让我们考虑一下,这些类是否等效? GetUser类返回数据,并且不更改服务器上的任何内容。 例如,这与请求“给我用户ID”有关。 UpdateEmail和BanUser类返回操作结果并更改状态。 例如,当我们告诉服务器:“请更改状态,您需要更改某些内容。”



让我们看一下HTTP协议。 有一个GET方法,根据HTTP协议的规范,该方法应返回数据而不更改服务器的状态。



还有其他方法可以更改服务器的状态并返回操作结果。



CQRS范例似乎是专门为HTTP协议设计的。 查询是GET操作,命令是PUT,POST,DELETE-无需发明任何东西。



我们重新定义处理程序并定义其他接口。 IQueryHandler,不同之处仅在于我们限制输入值的类型为IQuery。 IQuery是标记接口,除此泛型外,没有任何其他内容。 我们需要泛型以便将约束放在QueryHandler中,现在,声明QueryHandler时,我们不能不将Query传递到那里,而是将Query对象传递到那里,我们知道它的返回值。 如果只有一个接口,这将很方便,因此您不必在代码中寻找它们的实现,也可以避免麻烦。 您编写IQueryHandler,在其中编写实现,并且在TOut中不能替换其他类型的返回值。 它只是不编译。 因此,您可以立即看到哪些输入值对应于哪些输入数据。



对于CommandHandler来说,情况完全相似,但有一个例外:需要更多技巧才能使用这种通用方法,我们将进一步介绍。

处理程序实施


处理程序,我们宣布了,它们的实现是什么?



有什么问题吗? 似乎已失败。

装饰者急救


但这无济于事,因为我们仍然在中间,我们需要进一步完成一些工作,这一次,我们需要使用装饰器模式,即其出色的布局功能。 装饰器可以包裹在装饰器中,包裹在装饰器中,包裹在装饰器中-继续进行直到感到无聊为止。



然后一切将看起来像这样:有一个输入Dto,它输入第一个装饰器,第二个,第三个装饰器,然后我们进入Handler并退出它,遍历所有装饰器并在浏览器中返回Dto。 我们声明一个抽象基类以便以后继承,将Handler的主体传递给构造函数,然后声明一个抽象Handle方法,其中将悬挂其他装饰逻辑。



现在,借助装饰器,您可以构建整个管道。 让我们从团队开始。 我们有什么? 输入值,验证,访问权限验证,逻辑本身,由于该逻辑而发生的某些事件以及返回值。



让我们从验证开始。 我们声明一个装饰器。 类型T验证器的IEnumerable进入此装饰器的构造函数,我们全部执行它们,检查验证是否失败并且返回类型为IEnumerable<validationresult> ,然后可以返回它,因为类型匹配。 如果是其他Hander,那么您必须抛出一个Exception,因为这里没有结果,即另一个返回值的类型。



下一步是安全性。 我们还声明装饰器,创建CheckPermission方法并进行验证。 如果突然出了点问题,一切,我们不会继续。 现在,在我们完成所有检查并确保一切都很好之后,我们便可以履行我们的逻辑。

对原始语的痴迷


在展示该逻辑的实现之前,我想早一点开始,即从那里的输入值开始。



现在,如果我们选出这样一个类,那么大多数情况下它看起来可能像这样。 至少我在日常工作中看到的代码。



为了使验证生效,我们在此处添加一些属性,以告诉您验证的类型。 从数据结构的角度来看,这将有所帮助,但对于诸如检查数据库中的值之类的验证将无济于事。 只是EmailAddress,还不清楚如何,在何处检查如何使用这些属性以进入数据库。 除了属性,您可以转到特殊类型,然后将解决此问题。



代替int原语,我们声明一个具有泛型的Id类型,它是具有int键的某个实体。 我们要么将此实体传递给构造函数,要么传递其ID,但是同时我们必须传递一个由Id可以接受并返回的函数,检查那里是否为null。



我们对电子邮件也是如此。 将所有电子邮件转换为底线,以便一切对我们而言都一样。 接下来,我们使用Email属性,将其声明为静态以与ASP.NET验证兼容,在这里我们简单地将其称为。 也就是说,这也可以做到。 为了使ASP.NET基础结构能够捕获所有这些,您必须稍微修改序列化和/或ModelBinding。 那里没有太多的代码,它相对简单,所以我不会在这里停下来。



这些更改之后,将在此处显示专用类型,而不是原始类型:Id和Email。 在这些ModelBinder和更新的反序列化器解决之后,我们可以肯定地知道这些值是正确的,包括这些值在数据库中。 “不变式”



我要讲的下一点是类中的不变式的状态,因为经常使用贫血模型 ,其中只有一个类,有许多吸气剂,因此它们如何协同工作还不清楚。 我们使用复杂的业务逻辑,因此对代码进行自我记录对于我们而言很重要。 取而代之的是,最好为ORM声明真实的构造函数和空的构造函数,可以将其声明为受保护的,以便应用程序代码中的程序员无法调用它,而ORM可以。 在这里,我们传递的不是原始类型,而是Email类型,它已经正确正确了,如果为null,我们仍然抛出Exception。 您可以使用Fody,PostSharp,但是C#8即将推出,因此,将有一个Non-nullable引用类型,最好等待它在语言中的支持。 下一刻,如果我们想更改名称和姓氏,则很可能希望将它们一起更改,因此必须有一个适当的公共方法将它们一起更改。



在这种公共方法中,我们还验证了这些行的长度是否与我们在数据库中使用的行匹配。 如果出现问题,请停止执行。 在这里,我使用相同的技巧。 我声明一个特殊属性,然后在应用程序代码中调用它。



此外,此类属性可以在Dto中重用。 现在,如果要更改名称和姓氏,则可能会有这样的更改命令。 在这里值得添加一个特殊的构造函数吗? 这似乎是值得的。 它将变得更好,没有人会更改这些值,不会破坏它们,它们将完全正确。



其实不是。 事实是Dto根本不是真正的对象。 这是我们将反序列化数据放入的字典。 就是说,它们当然假装为对象,但是它们仅具有一种责任-必须进行序列化和反序列化。 如果我们试图与这种结构抗争,我们将开始与设计师宣布一些ModelBinder,这样做令人难以置信,而最重要的是,它将随着新框架的新发行而中断。 马克·西蒙(Mark Simon)在“程序的边界不是面向对象的”一文中对此进行了很好的描述,如果有趣的话,最好阅读他的文章,并对其进行详细描述。



简而言之,我们有一个肮脏的外部世界,我们在输入中进行检查,将其转换为干净的模型,然后将其全部转移回序列化,浏览器,再到肮脏的外部世界。

处理程序


完成所有这些更改之后,Hander的外观将如何?



为了方便阅读,我在此处写了两行,但通常可以将其写成一行。 数据是完全正确的,因为我们有一个类型系统,所以需要进行验证,即数据是钢筋混凝土的,因此您无需再次检查它们。 这样的用户也存在,没有其他用户拥有如此繁忙的电子邮件,一切都可以完成。 但是,仍然没有调用SaveChanges方法,没有通知,也没有日志和分析器,对吗? 我们继续前进。

大事记


域事件。



大概是Udi Dahan在他的文章“ Domain Events-Salvation”中第一次普及这个概念。 他建议在那里用Raise方法声明一个静态类,然后抛出此类事件。 稍后,Jimmy Bogard提出了一个更好的实现,称为“更好的域事件模式”



我将展示Bogard的序列化,其中有一个小变化,但很重要。 除了抛出事件外,我们还可以声明一些列表,并在实体内部直接发生某些反应的地方保存这些事件。 在这种情况下,此email程序也是User类,并且在该类中,此属性不假装为具有自动getter和setter的属性,但实际上为它添加了一些内容。 也就是说,这是真正的封装,而不是亵渎。 更改时,我们检查电子邮件是否不同并引发事件。 该事件尚未到达任何地方;我们仅在实体的内部列表中拥有它。



此外,在调用SaveChanges方法时,我们将使用ChangeTracker,以查看是否存在实现该接口的任何实体,以及它们是否具有域事件。 如果有的话,让我们接受所有这些域事件并将其发送给知道如何处理这些事件的调度程序。

此调度程序的实现是另一个讨论的主题,C#中的多重调度存在一些困难,但这也可以做到。 使用这种方法,还有另一个非显而易见的优势。 现在,如果我们有两个开发人员,一个可以编写更改此电子邮件的代码,另一个可以执行通知模块。 它们绝对不相互连接,它们编写不同的代码,仅在一个Dto类的此域事件级别连接。 第一个开发人员只是在某个时候放弃了该类,第二个开发人员对此做出了响应,并且知道需要通过电子邮件,SMS,将通知推送到电话以及所有其他上百万个通知来发送该类,同时考虑到通常发生的任何用户首选项。



这是最小但重要的一点。 Jimmy的文章使用了SaveChanges方法的重载,最好不要这样做。 最好在装饰器中执行此操作,因为如果我们重载SaveChanges方法并且需要在Handler中使用dbContext,则将获得循环依赖项。 您可以使用它,但是解决方案的便利性和美观性有所降低。 因此,如果管道是基于装饰器构建的,那么我认为没有理由做不同的事情。

记录和分析




代码的嵌套仍然存在,但是在最初的示例中,我们首先使用MiniProfiler,然后尝试catch,然后使用if。 总共有三层嵌套,现在每一层嵌套都在其自己的装饰器中。 在负责性能分析的装饰器内部,我们只有一层嵌套,可以完美地读取代码。 另外,很明显,在这些装饰器中,只有一种责任。 如果装饰者负责日志记录,那么他将仅记录日志(如果仅对概要文件进行概要分析),其他所有内容均位于其他位置。

回应


整个管道工作完毕后,我们只能将Dto传送给浏览器,并序列化JSON。



但是还有一点小事情,有时会忘记:在每个阶段,这里都可能发生异常,实际上您需要以某种方式处理它们。



我不得不在这里再次提及Scott Vlashin和他的报告“面向铁路的编程” 。 怎么了 原始报告完全致力于处理F#语言中的错误,如何以不同的方式组织流程以及为什么这样的方法可能比使用Exception'ov更可取。 在F#中,这确实很好用,因为F#是一种功能语言,而Scott使用一种功能语言的功能。



由于可能大多数人仍使用C#编写代码,因此,如果使用C#编写类似代码 ,则这种方法将类似于以下内容。 而不是抛出异常,我们声明一个Result类,该类具有成功的分支和不成功的分支。 据此,两名设计师。 一类只能处于一种状态。 此类是联合体类型的特殊情况,已将联合体与F#区别开来,但由于在C#中没有内置支持,因此用C#重写。



使用模式匹配,而不是声明公共获取者某人可能不会在代码中检查null。 再次,在F#中它将是一种内置的模式匹配语言,在C#中,我们必须编写一个单独的方法,我们将向其中传递一个函数,该函数知道如何处理成功的操作,如何将其进一步转换到链上以及存在错误。 也就是说,无论哪个分支对我们有用,我们都必须将其强制转换为单个返回结果。 在F#中,所有这些都很好地工作,因为这里有功能组成,还有我已经列出的所有其他内容。 在.NET中,这种方法的工作情况稍差一些,因为一旦您拥有多个Result,但是很多-几乎每种方法都可能由于一种或另一种原因而失败-几乎所有结果函数类型都将成为Result类型,并且您需要它们作为结合一些东西。



组合它们的最简单方法是使用LINQ ,因为实际上LINQ不仅适用于IEnumerable,如果以正确的方式重新定义SelectMany和Select方法,则C#编译器将看到可以对这些类型使用LINQ语法。 通常,结果是使用Haskell do标记或在F#中具有相同的计算表达式的描图纸。 应该如何阅读? 在这里,我们有三个运算结果,如果在这三种情况下都没问题,则取这些结果r1 + r2 + r3并将其相加。 结果值的类型也将是Result,但是新的Result(我们在Select中声明的)。 总的来说,这甚至是一种可行的方法,即使不是。



对于所有其他开发人员,一旦开始使用C#编写此类代码,您就会开始看起来像这样。 “这些都是可怕的可怕异常,请不要写! 他们是邪恶的! 更好地编写没人能理解和调试的代码!”



C#不是F#,它有所不同,在此基础上并没有不同的概念,而且当我们尝试在地球上拉猫头鹰时,事实证明,这有点不寻常。



相反,您可以使用已记录的内置普通工具 ,这些工具众所周知且不会在开发人员之间引起认知失调。 ASP.NET具有全局处理程序异常。



我们知道,如果验证存在任何问题,则需要返回代码400或422(不可处理实体)。 如果身份验证和授权存在问题,则为401和403。如果出了问题,则出了问题。 如果出了什么问题,并且您想告诉用户确切的信息,请定义您的Exception类型,说它是IHasUserMessage,在此接口中声明一个Message getter并检查:如果实现了此接口,则可以接收一条消息从Exception并将其以JSON传递给用户。 如果未实现此接口,则意味着存在某种系统错误,我们只是告诉用户出了点问题,我们已经在这样做了,我们都知道-和往常一样。

查询管道


我们与团队一起得出结论,并看一下读堆栈中的内容。 至于请求,验证,直接响应-这是同一件事,我们不会单独停止。 可能仍然会有一个额外的缓存,但是总体而言,缓存也没有大问题。

安全性


让我们更好地进行安全检查。 也可能有相同的Security装饰器,它检查是否可以发出此请求:



但是在另一种情况下,我们显示多个记录并列出,对于某些用户,我们需要显示完整的列表(例如,对于某些超级管理员),对于其他用户,我们必须列出有限的列表,第三-有限对于另一种情况(通常在公司应用程序中很常见),访问权限可能非常复杂,因此您需要确保不以这些用户为目标的数据不会进入这些列表。

这个问题很简单地解决了。 我们可以重新定义原始可查询对象到达和可查询返回对象的接口(IPermissionFilter)。 区别在于,对于返回的查询,我们已经施加了附加条件,其中检查了当前用户并说:“在这里,仅向该用户返回该数据……”-然后是与权限相关的所有逻辑。 同样,如果您有两个程序员,则一个程序员去写权限,他知道他只需要编写很多PermissionFilter,并检查它们对于所有实体是否正常工作。 其他程序员对权限一无所知,在他们的列表中,正确的数据总是总是通过,仅此而已。 因为它们不再从输入中接收来自dbContext的原始查询,而是限于过滤器。 这样的PermissionFilter还具有布局属性,我们可以添加并应用所有PermissionFilters。 结果,我们得到了结果权限过滤器,该过滤器将考虑到适合该实体的所有条件,将数据选择范围缩小到最大。



为什么不使用ORM内置工具(例如,实体框架中的全局过滤器)呢? 同样,为了不让自己产生任何循环依赖性,也不要将有关业务层的任何其他故事拖到上下文中。

查询管道。 读取模型


剩下的要看阅读模型。 CQRS范例不使用阅读堆栈中的域模型,而是直接创建浏览器当前所需的Dto。



如果我们用C#编写,那么很可能我们正在使用LINQ,如果不仅有任何苛刻的性能要求,而且有任何要求,那么您可能没有企业应用程序。 通常,使用这样的LinqQueryHandler可以彻底解决此问题。 这是对泛型的一个相当可怕的约束:这是Query,它返回投影列表,并且它仍然可以过滤这些投影并对这些投影进行排序。 她还仅处理某些类型的实体,并且知道如何将这些实体转换为投影并将这种投影列表以Dto的形式返回给浏览器。



Handle方法的实现可能非常简单。 以防万一,请检查此TQuery过滤器是否为原始实体实现。 进一步我们做一个投影,它是可查询扩展名AutoMapper'a。 如果仍然不知道,AutoMapper可以在LINQ中构建投影,即那些将构建Select方法而不将其映射到内存中的投影。

然后,我们将过滤,排序并全部显示在浏览器中。 , DotNext, , , , , , expression' , .

SQL


. , DotNext', — SQL. Select , , , queryable- .



, . , Title, Title , . , . SubTitle, , , - , queryable- . , .

, . , , . , , . «JsonIgnore», . , , Dto. , , . JSON, , Created LastUpdated , SubTitle — , . , , , , , . , - .



. , -, , . , pipeline, . — , , . , SaveChanges, Query SaveChanges. , , , NuGet, .

. , - , , . , , , , , — . , , : « », — . .


, ?



- . .



, , , . MediatR , . , , — , MediatR pipeline behaviour. , Request/Response, RequestHandler' . Simple Injector, — .



, , , , TIn: ICommand.



Simple Injector' constraint' . , , , constraint', Handler', constraint. , constraint ICommand, SaveChanges constraint' ICommand, Simple Injector , constraint' , Handler'. , , , .

? Simple Injector MeriatR — , , Autofac', -, , , . , .

,


, «».



, «Clean architecture». .



- - , MVC, , .



, , , Angular, , , , . , : « — MVC-», : « Features, : , Blog - Import, - ».

, , , , MVC-, , - , . MVC . , , — . .





- , - -, .

-, , . , . , - , User Service, pull request', , User Service , . , - , - , . - , .

. , . , , , . , , , , , , , - . , ( , ), , «Delete»: , , . .

— «», , , , . , : , , , . , . , , . , , .

: . « », : , , . , , , , , , , . , . , - pull request , — , — - , . VCS : - , ? , - , , .



, , , . : . , . , , , , . , , , . , , . « », , . , , — , , .

: , - , . . - , , , , . - , - , , , , . .



. , IHandler . .

IHandler ICommandHandler IQueryHandler , . , , . , CommandHandler, CommandHandler', .

为什么这样 , Query , Query — . , , , Hander, CommandHandler QueryHandler, - use case, .

— , , , , : , .

, . , . , -.

C# 8, nullable reference type . , , , , .

ChangeTracker' ORM.

Exception' — , F#, C#. , - , - , . , , Exception', , LINQ, , , , , , Dapper - , , , .NET.

, LINQ, , permission' — . , , - , , . , — .

. :






— . . — «Domain Modeling Made Functional», F#, F#, , , , , . C# , , Exception'.

, , — «Entity Framework Core In Action». , Entity Framework, , DDD ORM, , ORM DDD .

分钟的广告。 15-16 2019 .NET- DotNext Piter, . , .

Source: https://habr.com/ru/post/zh-CN447308/


All Articles