我的 .NET Core 博客性能优化经验总结

点击上方蓝字关注“汪宇杰博客”

导语

去年8月,我用 .NET Core 重写了我的博客系统。经过一年多的优化,服务器响应速度从上线时候的 80ms 提高到了现在的 8ms,十倍提速。可惜由于部署在国外,自然不可抗力会导致中国用户晚上访问速度不稳定。本文分享网络正常的前提下,我做了哪些优化和提升,希望能帮到大家。

其实,在.NET Core之前,我的旧版博客系统是 .NET Framework写的,从2008年的 ASP.NET Web From 2.0 一直维护到2018年的 ASP.NET MVC5,曾经被人怀疑过:“别骗人了,.NET怎么可能这么快?” 。而如今,.NET Core 从本质上就已经比 .NET Framework 有了巨大的性能提升,甚至在不少测试下超过了Node、Go、Java。其实光看 benchmark 没太大的意义,大部分实际应用中性能问题并不在于语言和框架,而是由不佳的设计、错误的框架使用方法引起的。在 .NET Core 的实践过程中,我也学习和收获了很多,因此写下此文,分享我自己的性能优化经验。

没有银弹

首先,每个系统都是不同的。性能优化需要针对不同系统,不同业务场景,不同应用领域,不同用户种群,没有一个通用方法。比如我的博客,是内容站,交互少,大量情况都是各种姿势读数据,所以我要保证的是尽可能快的提升数据读取速度。而有些系统,比如电商,有远比内容站复杂的业务逻辑,还有秒杀等极端情况。比如国内阿里带队的“数据库不要有外键”,这是因为阿里的业务压力必须这么做,他们需要的是极端情况的写入速度,显然我的博客以及很多内容站没有这种场景,因此我依然可以用外键。所以,在开始之前,读者必须明白,软件设计是没有银弹的。我所列出的经验仅仅针对我自己的博客。大部分经验能应用在类似的内容站上,但不要盲目实践。同样是内容站,面对的用户群和压力也不一样,比如我的博客肯定无法和新浪、网易等比流量,所以优化的关键点和方法也不同。

分析和发现关键点

虽然我们在系统设计时会有一定的预判,比如哪些功能是用户最常用的,哪些请求会是最频繁的。但是上线之后用户的行为才是事实,有时候系统的表现会和我们的预期不一样。而且,随着时间的推移,用户的使用习惯可能会变,系统面临压力的部分也会改变。所以,我们需要记录和分析系统在实际使用过程中产生的数据和用户行为。而我所使用的Azure Application Insights就是一款极佳的APM工具。作为一个网站,性能是服务端(后台)和客户端(前台)共同决定的,Azure Application Insights可以同时收集后端API处理速度、数据库查询相应速度以及前端页面资源加载速度、JS执行速度等,也会自动分析出最慢的请求是哪些,系统最耗时的操作在哪个环节(前端、程序或数据库),甚至Azure SQL Database能根据实际使用情况自动推荐优化方案(比如哪里加何种索引等)。本文不讨论APM工具的使用。但是做性能优化的时候,必须针对实际用户产生的数据,分析以后去鉴别哪里需要优化。我的博客上线几个月后,我的分析如下:

1.       客户端性能开销在加载资源和过多的请求(前端库,博客文章配图)

2.       服务端性能开销在过多重复的SQL查询

3.       博客配图由后端从Azure Blob Storage中读取再返回前端产生双倍性能开销

前端实践

使用 bundle 避免过多请求

我相信大部分Web程序员都熟悉这一条建议,这也是最直接有效的前端性能提升方式。我们网站中通常要加载许多不同的库和资源,有图片,CSS,JS等。而浏览器大量的时间开销在于对这些资源发起请求,等待响应。即使你的文件很小,但是太多的请求数量会明显降低网页加载速度。因此很久之前业界就流行一种做法,即打包压缩资源文件,比如将多个JS文件打包压缩成一份,这样浏览器就只要发起一个请求,就能加载你网站所有需要的JS资源。

打包工具五花八门,可以根据自己的喜好选择。我博客使用的是 BuildBundlerMinifier,它可以在编程和编译时完成打包:

<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />

其定义示例如下:

{

  "outputFileName": "wwwroot/js/app/app-js-bundle.js",

  "inputFiles": [

    "wwwroot/lib/jquery/jquery.min.js",

    "wwwroot/lib/jquery-validate/jquery.validate.min.js",

    "wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",

    "wwwroot/lib/twitter-bootstrap/js/bootstrap.bundle.min.js",

    "wwwroot/lib/jquery-qrcode/jquery.qrcode.min.js",

    "wwwroot/lib/toastr.js/toastr.min.js",

    "wwwroot/js/lazyload.js",

    "wwwroot/js/app/moonglade-base.js",

    "wwwroot/js/app/postslug.js",

    "wwwroot/js/app/csrf.js",

    "wwwroot/js/app/comments.js"

  ]

}

Js真的要放body最后吗?

这也是一条几乎Web程序员人尽皆知的原则。如果你将JS资源放在body最后加载,即</body>标签之前,那么浏览器会异步加载你的JS。如果按照传统方式将JS资源放在head标签里,那么浏览器必须加载完JS资源才开始渲染网页。

聪明的朋友可能了解,这一条在2019年已经不一定适用了。首先,我们可以通过添加defer标签来告诉浏览器,遇到这个JS,不要等加载完成再继续干活,你管你渲染网页,我管我加载:

<script defer src="996.js"></script>

<script defer src="007.js"></script>

不过defer的脚本还是会按顺序执行,这对于有依赖关系的JS资源十分重要,比如上面这段代码,即使007.js非常小,首先加载完成,它也必须等到996.js加载完成后才能执行。如果你想要谁先加载完,谁先执行的效果,把defer换成async即可,这种情况下你得保证你的JS之间没有依赖关系,没有依赖关系,没有依赖关系!!!重要的说三遍!

可惜,由于我们控制不了用户使用的浏览器类型和版本,根据 Azure Application Insights 的后台统计,仍然有不少用户使用低版本的浏览器访问我的网站,它们并不认识 defer和 async。

所以目前,我博客的实践依然是JS尽量放body最后,但不是绝对!由于框架性质的JS文件必须完成加载才能正确渲染网页,因此我博客中它们还是放在head里,而用户代码我会放在body最后。优化性能的前提,一定是不要影响正常功能!所以,程序员看问题不要非黑即白,还是那句软件工程的老话:没有银弹。

如果你的网站没有低版本的客户端,那么可以尽量用 defer和 async。

使用 HTTP/2

启用HTTP2可以有效提高网络传输效率,根据该项调研(https://w3techs.com/technologies/details/ce-http2),截至2019年12月,全球大约有42.6%的网站已经升级到了HTTP2。其对于网络性能的提升主要在这几个方面:

降低延迟以提高网页加载速度:

  • HTTP头的数据压缩

  • 服务器端推送 (这个.NET Core好像没有)

  • 请求管线

  • 修复HTTP 1.x中head-of-line blocking 的问题

  • 同一个TCP连接上的请求多路复用

(参考:https://en.wikipedia.org/wiki/HTTP/2)

而我的博客使用微软 Azure  App Service 托管,可以点点鼠标一秒切换到 HTTP/2,而不用自己996收福报:

如果你没有用 Azure,也不用担心,最新版 .NET Core 3.1 的kestrel 默认就打开了HTTP/2:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-3.1#http2-support

使用压缩

开启服务器端response压缩可以减小资源传输的体积,从而达到提升性能的目的。使用 ASP.NET Core 开发的网站,部署在Azure上默认就会开启gzip,不需要自己996去研究。我的博客采用的 App Service Plan 是 Windows Server 2016,上面的IIS启用了静态和动态资源压缩。

然而,如果你不幸没有使用 Azure,那么自己稍微996一下,在IIS上开启压缩也不难,可以点点鼠标就搞定,也可以通过Web.config开启(.NET Core部署在IIS下也认web.config),具体方法可以参考:https://docs.microsoft.com/en-us/iis/configuration/system.webserver/httpcompression/  

如果你用的不是IIS,也没关系,再996一下,.NET Core自己也可以加response压缩:

https://docs.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-3.1

真的要用SPA吗?

2014年以后,随着SPA的兴起,Angular等框架逐渐成为了前端开发的主流。它们解决的问题正是提升前端的响应度,让Web应用尽量接近本地原生应用的体验。我也遇到过不少朋友有疑问,为啥我的博客不用angular写?是我不会吗?

其实并不那么简单。实际上我在公司的主要工作目前也是写angular,博客曾经的.NET Framework版的后台也用过angularjs以及angular2,经过一系列的实践表明,我博客这样的内容站用angular收益并不大。

其实这并不奇怪,在盲目选择框架之前,我们得注意一个前提条件:SPA框架所针对的,其实是Web应用。而应用的意思是重交互,即像Azure Portal或Outlook邮箱那样,目的是把网页当应用程来开发,这时候SPA不仅能提升用户体验,也能降低开发成本,何乐而不为?但是博客属于内容为主的网站,不是应用,要说应用也勉强只能说博客的后台管理可以是应用。博客前台唯一的交互就是评论、搜索,因此SPA并不适合这样的工作。这就像你要去菜场买菜,骑自行车反而比你开个坦克过去方便。

所以,程序员切记,看待问题不要非黑即白,不要觉得什么流行就一定适合所有项目,还是那个著名的软件工程原则:没有银弹!

在微软官方文档里也有同样的关于何时选择SPA,何时选择传统网站的参考:

https://docs.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/choose-between-traditional-web-and-single-page-apps 

You should use traditional web applications when:

  • Your application's client-side requirements are simple or even read-only.

  • Your application needs to function in browsers without JavaScript support.

  • Your team is unfamiliar with JavaScript or TypeScript development techniques.

You should use a SPA when:

  • Your application must expose a rich user interface with many features.

  • Your team is familiar with JavaScript and/or TypeScript development.

  • Your application must already expose an API for other (internal or public) clients.

后端实践

尽量避免Exception

.NET的Exception是一种特殊的类型,不管用户代码是否处理exception,只要产生,就会在CLR上有开销。所以尽量避免产生Exception,尤其是不要利用Exception控制程序流程,这一点通常在.NET的技术文章里都会提及。一个不正常利用Exception的例子是我曾经在公司代码里看见过类似这样判断输入的内容是否为数字的代码:

try

{            

      Convert.ToInt32(userInput);

      return true;

}

catch (Exception ex)

{

      return false;

}

而.NET其实可以这样写:

int.TryParse(userInput);

我相信大部分正常的.NET程序员都不会犯上面这种错误。这样的代码效率低下且不说,还容易炸毁IIS。IIS的应用程序池如果在短时间检测到大量CLR异常就会自爆重启并返回503,中断你的网站服务。

不过关于Exception的另一个争论点在于,是否需要为业务异常设计自己的Exception类型?也就是检查到非正常业务行为,到底返回Error Code还是直接抛出Exception再由上层处理?关于这点,我也没有确定的结论。目前我的实践是,仅对于非法输入抛出参数异常,业务上的错误不抛异常,例如文章被和谐后产生的404,不去设计比如 PostNotFoundException,这一点很关键,因为经常有无聊黑客新手使用自动化工具扫描我的博客是否有漏洞,而这些工具会批量请求例如wp-login.php之类的对于我博客来说不存在的资源,如果我设计成抛出Exception再返回404,那么会造成短时间内CLR上大量的异常,绝对会爆。

参考:https://devblogs.microsoft.com/cbrumme/the-exception-model/ 中“Performance and Trends”一节。

EF尽量使用AsNoTracking筛选只读数据

每个.NET群,都可以为Entity Framework vs Dapper吵一天。其实EF虽然在很多场景由局限,但并不那么差,只是想要用对,不产生性能问题,付出的学习成本相当高。但是既然入坑了,就最好把它用用对。而最常见的情况就是遇到只读数据,可以加上AsNoTracking()。我博客大部分的场景都是只读数据,并且读取后直接处理好关联数据(Include),因此可以使用AsNoTracking()来断开EF对于对象的追踪,节省内存也提高性能。为了不每次手写AsNoTracking() 导致996,我在博客的存储层直接设置了默认参数:

public IReadOnlyList<T> Get(ISpecification<T> spec, bool asNoTracking = true)

{

    return asNoTracking ?

        ApplySpecification(spec).AsNoTracking().ToList() :

        ApplySpecification(spec).ToList();

}

关于EF,我在2012年还写过一篇关于性能的文章,至今也适用于.NET Core,欢迎参考:

《Performance tips for Entity Framework》

另外,在最新的EF Core 3.x中,微软为了不被人骂EF性能差,直接默认禁止了client side evaluation,避免了忘写Include结果还开Lazy Load导致外键表被查询几百次的尴尬场面。

数据库DTU

我的博客采用Azure SQL数据库的DTU计量方式。请求频繁的时候会导致DTU耗尽,从而后续请求需要排队执行。所以首先优化的就是增加DTU容量,目前20个DTU基本管够。

而DTU是否够用可以直接在Azure的面板里看报表得到:

内存及缓存,减少数据库调用

计算机的内存是为了用,而不是为了省。程序要么牺牲空间换时间,要么牺牲时间换空间。合理使用内存做缓存,而不是每次都调用数据库,可以提高一段时间内的性能。特别是云端环境,数据库的调用通常是最花时间的环节(Application Insights里认为是dependency call)。即使不用内存缓存,也可以根据项目需要配置redis等产品。

在我博客里,缓存的使用随处可见。比如文章分类、Custom Page这种不经常更新的数据,就可以缓存起来,这样就不至于每次请求都去查询数据库。另外,像配置之类的数据,也建议设计成单例模式,网站启动时候加载完毕,不要每个请求都去数据库里重新读配置。这将极大的减少数据库的压力并提高网站响应速度。

var cacheKey = $"page-{routeName.ToLower()}";

var pageResponse = await cache.GetOrCreateAsync(cacheKey, async entry =>

{

    var response = await _customPageService.GetPageAsync(routeName);

    return response;

});

除了数据库,本地、远程图片或其他类型的文件也可以利用缓存来提高性能。

CDN

尽量用CDN服务静态资源,并配置pre-fetch,减少DNS解析次数。我的博客图片由于设计了抽象隔离,博客的配图并不是像访问静态资源那样直接输出到客户端,目前支持两种存储方式:Azure Blob、本地文件系统,不管哪种存储,都避免不了从对应位置读取图片,并返回给客户端显示,即使加上了服务器端缓存(MemoryCache),这个过程也依然对服务器有较大压力。

目前我选用的存储方式为Azure Blob。以前读取一张图片的过程是:

首次请求:服务器去Azure Blob拿图片,客户端再去网站服务器拿图片。

后续请求:Hit到memory cache,仅从网站服务器返回图片给客户端。

然而,即使后续请求不用经过Azure Blob,对Web服务器的请求还是必须存在的,这也是挺大的开销。于是,我通过CDN,让图片请求再也不经过我自己的Web服务器,而是直接访问Azure Blob。

于是现在,读取一张图片的过程是:

首次请求:CDN判断自己是否已经缓存了图片,如果没有,去Azure Blob里拿,并缓存起来。

首次请求:CDN判断自己是否已经缓存了图片,如果没有,去Azure Blob里拿,并缓存起来。

这样一来,用户阅读博客文章时产生的图片请求只会经过Azure CDN的服务器,不会对Web服务器造成压力。

另外,可以在网页 header 上加个 dns-prefetch,指向CDN服务器域名:

<link rel="dns-prefetch" href="https://cdn-blob.edi.wang" />

这样浏览器就会提前解析CDN服务器的地址,进一步加快网页加载速度。

日志级别

很多程序员习惯本地和生产用同一份日志配置,而本地通常打开Debug、Trace等低等级日志以帮助我们的开发和测试工作,线上的产品是经过测试的相对稳定的发布版本,其实并不需要这些低等级日志,所有的事件都要记log的话会极大的影响应用性能。所以我的实践是生产环境只开Warning以上的日志级别,除非遇到刁钻问题需要收集详细爆炸数据,会临时开几个小时的Debug日志。

APM不要随便加profiler

这条建议和上面类似,APM工具通常提供了各种profiler,然而这一般都会影响性能。就算是Azure自己的Application Insights也是如此。所以除非程序出现需要996调查的爆炸事故,一般不建议打开这些profiler。

总结

以上是我目前使用到的提升博客性能的方法。但是性能优化没有完全通用的策略,需要根据不同系统,不同业务,不同压力来动态调整优化方案,总体思想即:减少不必要的调用与开销。但有时候也需要调整应用程序的部署架构,比如Azure可以加上Traffic Manager、Front Door,使用负载均衡功能。欢迎大家留言分享自己的想法,以及对本文的补充和建议!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis 6 RC1发布,带来众多新特性

Redis 6 RC1 发布了&#xff0c;项目创建人 antirez 在博客中介绍&#xff0c;这是迄今最“企业”化的版本&#xff08;SSL 与 ACL 等特性与企业极相关&#xff09;&#xff0c;也是最大的版本&#xff0c;同时也是参与人数最多的版本。GA 版本预计在明年三月到五月之间发布。R…

Serverless那么火,2019年的采用如何?

传统的 IT 架构已经不适合当今快速发展的数字经济环境。技术顾问 Declan Morris 表示&#xff1a;“客户不仅期望零停机时间&#xff0c;而且要求应用程序无论在世界任何地方都具有一致的性能。”他还预测到&#xff0c;接下来是 Serverless 计算和函数即服务&#xff08;FaaS&…

【C】Natasha 插件编程

文章转载授权级别&#xff1a;CNatasha 是一个代替 Emit / Expression 的动态构建项目&#xff0c;旨在为开发者提供方便、快捷、高性能的动态构建服务&#xff0c;动态编程是生态的重要组成部分&#xff0c;希望开发者们能有足够的兴趣来了解、使用、建设它。1、插件生成使用 …

二叉树先序,中序,后序,层次遍历(数据结构)

先序遍历 先序遍历可以想象为&#xff0c;一个小人从一棵二叉树的根节点为起点&#xff0c;沿着二叉树的外沿&#xff0c;逆时针走一圈回到根节点&#xff0c;路上遇到的元素顺序&#xff0c;就是先序遍历的结果 先序遍历的结果为&#xff1a;A B D H I E J C F K G 中序遍…

.NET Core 在 K8S 上的开发实践--学习笔记

摘要本主题受众是架构师&#xff0c;开发人员&#xff0c;互联网企业 IT 运维人员。大纲&#xff1a;1、 K8S 对应用的要求&#xff1b;2、 .NET Core 上 K8S 的优势&#xff1b;3、 K8S 下的 .NET Core 配置&#xff1b;4、 .NET Core 上分布式组件概览。大纲kubernetes 对应用…

.NetCore 3.1 安装本地化中文智能提示

A、平时在群里&#xff0c;很多小伙伴都会问&#xff0c;.net core的智能提示和注释都是英文的&#xff0c;如果英语水平不是很高的&#xff0c;看着是挺麻烦&#xff0c;所以经常需要在身边有一个翻译软件&#xff0c;如果有汉化的中文智能提示就好了&#xff08;当然&#xf…

asp.net core 3.x Endpoint终结点路由1-基本介绍和使用

前言我是从.net 4.5直接跳到.net core 3.x的&#xff0c;感觉asp.net这套东西最初是从4.5中的owin形成的。目前官方文档重点是讲路由&#xff0c;没有特别说明与传统路由的区别&#xff0c;本篇主要介绍终结点路由的相关概念和如何使用&#xff0c;不会详细介绍路由&#xff0c…

共享后缀的链表

有一种存储英文单词的方法&#xff0c;是把单词的所有字母串在一个单链表上。为了节省一点空间&#xff0c;如果有两个单词有同样的后缀&#xff0c;就让它们共享这个后缀。下图给出了单词“loading”和“being”的存储形式。本题要求你找出两个链表的公共后缀。 函数接口定义&…

C#反射与特性(一):反射基础

1. 说明1.1 关于反射、特性在 《C# 7.0 本质论》中&#xff0c;关于这方面的知识在 《第十八章 反射、特性和动态编程》&#xff1b;在《C# 7.0 核心技术指南》中&#xff0c;这部分内容在《第19章 反射和元数据》。[图片来自 《C# 7.0 本质论》]在这里我们可以获得一些关联性很…

收藏!推荐12个超实用的Visual Studio插件

工欲善其事&#xff0c;必先利其器,整理的一些我必装的12款Visual Studio插件&#xff0c;希望你们能get到。效率工具前文传送门&#xff1a;推荐&#xff1a;程序员必装的10款谷歌插件程序员必备的8个学习工具99%的人不知道搜索引擎的6个技巧01 CodeMaidCodeMaid快速整理代码文…

搭建独立博客,这款评论插件不能错过

微信公众号因为申请的时间晚&#xff0c;一直到现在都无法开通评论功能&#xff0c;之前博客一直使用的多说作为评论系统&#xff0c;自从多说关闭后&#xff0c;好多年都处于无评论状态&#xff0c;最近发现 gitalk 还不错&#xff0c;所以在博客中进行了对 gitalk 的集成&…

最大堆和最小堆(数据结构)

堆和栈的区别&#xff1a; 一、空间分配区别&#xff1a; 栈&#xff08;操作系统&#xff09;&#xff1a;由操作系统自动分配释放&#xff0c;存放函数的参考值&#xff0c;局部变量的值等。其操作方式类似于数据结构中的栈堆&#xff08;操作系统&#xff09;&#xff1a;一…

2019公众号总结之——Top100 技术文章汇总

大家好&#xff0c;我是张善友。新年伊始&#xff0c;我们在欢送10年代的同时迎来了20年代。在这个崭新的时代&#xff0c;感谢各位朋友一直关注“dotnet跨平台”。一晃5年有余&#xff0c;关注公众号的粉丝6万&#xff0c;相对于.NET开发人员&#xff0c;希望2020年有更多小伙…

树,森林,二叉树的互相转换

树、森林到二叉树的转换 将树转换为二叉树 树中每个结点最多只有一个最左边的孩子&#xff08;长子&#xff09;和一个右邻的兄弟。按照这种关系很自然地就将树转换成相应的二叉树&#xff1a; 在所有兄弟结点之间加一连线对每个结点&#xff0c;除了保留与其长子的连线外&am…

数据丢失引起宕机怎么办?

做过系统开发和运维的朋友&#xff0c;应该最怕数据丢失问题出现&#xff0c;更严重的是造成无法恢复的糟糕境地&#xff0c;简直叫人崩溃啊&#xff0c;这周有一个朋友跟我咨询这方面的事情&#xff0c;就整理了一下数据库自动异地备份的方法&#xff0c;分享给大家。大家都知…

ASP.NET Core跨平台技术内幕

ASP.NET Core设计初衷是开源跨平台、高性能Web服务器&#xff0c;其中跨平台特性较早期ASP.NET是一个显著的飞跃&#xff0c;.NET现可以理直气壮与JAVA同台竞技&#xff0c;而ASP.NET Core的高性能特性更是成为致胜法宝。ASP.NET Core 2.1为IIS托管新增In-Process模型并作为默认…

使用 Visual Studio Code 进行远程开发

在完成了 AT 指令入门的学习之后&#xff0c;接下来就要使用 AT 指令进行 Socket 通信了。问题在于&#xff0c;之前 .NET 的 Socket 编程只需一台电脑便可进行学习&#xff0c;服务器和客户端都可以在本机运行&#xff0c;也可以分别运行在局域网上的两台电脑之上。而 NB-IOT …

.NETCore3.1中的Json互操作最全解读-收藏级

前言本文比较长&#xff0c;我建议大家先点赞、收藏后慢慢阅读&#xff0c;点赞再看&#xff0c;形成习惯&#xff01;我很高兴,.NETCore终于来到了3.1LTS版本&#xff0c;并且将支持3年&#xff0c;我们也准备让部分业务迁移到3.1上面&#xff0c;不过很快我们就遇到了新的问题…

逻辑结构的四种基本关系

逻辑结构的四种基本关系 1集合结构&#xff1a;数据元素之间除了“属于同一集合”的关系外&#xff0c;没有其他关系 2线性结构&#xff1a;数据元素之间存在一对一的关系 3树结构&#xff1a;数据元素之间存在一对多的关系 4图结构&#xff1a;数据元素之间存在多对多的关系

轻量级开源小程序SDK发车啦

Magicodes.WxMiniProgram.Sdk轻量级微信小程序SDK&#xff0c;支持.NET Framework以及.NET Core。目前已提供Abp模块的封装&#xff0c;支持开箱即用。地址&#xff1a;https://github.com/xin-lai/Magicodes.WxMiniProgram.SdkNuget新的包主要功能轻量级微信小程序SDK&#xf…