针对ASP.NET Core Web API的先进架构

.NET Core 最初是在2016年发布的,随着.NET Core 2.0的发布,微软拥有了下一个通用、模块化、跨平台和开源的平台主版本。.NET Core已经创建了许多API,在当前版本的.net框架中均可用。它最初是为下一代ASP.NET解决方案而创建的,但现在成了许多其他场景的驱动和基础,包括物联网、云计算和下一代移动解决方案。在本系列文章中,我们将探讨.NET Core的一些好处,以及它如何不仅能使传统的.NET开发人员受益,还能使所有需要为市场带来健壮、高效和经济的解决方案的技术人员受益。

今天的互联网与五年前已经完全不同了,更不用说20年前我刚开始做专业开发人员的时候了。天,Web api连接了由Web应用和移动应用驱动的现代互联网。有一种技能非常需要,那就是创建其他开发人员也可以使用的健壮的Web api。驱动大多数现代web和移动应用的API都需要具有稳定性和可靠性,以便在流量达到性能限制时仍能继续服务。

本文的目的是描述ASP.NET Core 2.0 Web API解决方案的体系结构,它使用了Hexagonal架构和端口和适配器模式。首先,我们来看看.NET Core和ASP.NET Core的新特性,它们对现代Web API很有帮助。

本文示例中的解决方案和所有代码都可以在我的GitHub存储库 ChinookASPNETCoreAPIHex中找到。

用于Web API的.NET Core 和 ASP.NET Core

ASP.NET Core是微软在.NET Core的基础上构建的一个新的web框架,用来摆脱.NET 1.0以来的遗留技术。相比之下,ASP.NET 4.6仍然使用System.Webassembly(它包含了所有WebForms类库),也因此引入了最近的ASP.NET MVC 5解决方案。通过摆脱这些遗留依赖和从头开始开发框架,并为跨平台执行进行了架构设计,ASP.NET Core 2.0为开发人员提供了更好的性能。使用ASP.NET Core 2.0,你的解决方案将在Linux上和Windows上均可有效运转。

更多关于.NET Core和ASP.NET Core的好处,你可以阅读本系列的其他三篇文章。第一篇是Maarten Balliauw的《性能是.NET的核心特性》、Chris Klug的《ASP.NET Core --简洁的力量》,以及最后Eric Boyd的《Azure和.NET Core的完美契合》。

体系架构

构建一个优秀的API依赖于伟大的架构。我们将从ASP.NET Core 的内置功能来研究API设计和开发的许多方面以形成哲学和最终设计模式的架构。这个架构背后有很多计划和想法,让我们开始吧。

依赖注入

在我们深入研究ASP的架构之前.NET Core Web API解决方案,我想讨论一下我所认为的使.NET核心开发人员过得更好的单一好处;即依赖注入(DI)。现在,我知道你会说我们在.NET框架和ASP.NET解决方案中有依赖注入。我同意,但是我们过去使用的依赖注入是来自第三方的商业提供商或者开源库。他们做得很好,但是对于.NET开发人员来说,有一个很陡峭的学习曲线,并且所有的依赖注入库都有自己独特的处理方法。今天,有了.NET Core,我们从一开始就将依赖注入集成到框架中了。此外,它的用法非常简单,它是立即可用的。

我们需要在API中使用依赖注入的原因是,它允许我们有最好的经验来解耦架构层,并允许我们模拟数据层,或者为API构建多个数据源。

要使用.NET Core 依赖注入 框架,请确保您的项目引用了Microsoft.AspNetCore.AllNuGet包(它包含对Microsoft.Extnesions.DependencyInjection.Abstractionspackage的依赖关系)。这个包提供了对IServiceCollection接口的访问,该接口具有一个System.IServiceProvider 接口,您可以调用GetService<TService>从IServiceCollection接口获得所需的服务,需要添加项目所需的服务。

要了解更多关于.NET Core 依赖注入的信息,我建议您阅读以下关于MSDN的文档:ASP.NET中依赖注入的介绍。

现在我们来看看为什么要像我一样做API架构设计的原理。设计任何架构的两个方面都依赖于这两个概念:允许深度可维护性,以及在解决方案中使用经过验证的模式和架构。

API的可维护性

对于任何工程过程来说,可维护性是指一个产品易于被维护:发现缺陷、纠正发现的缺陷、无需替换仍在工作的组件即可修复或更换有缺陷的组件、预防意外故障、最大限度地提高产品的使用寿命、有能力满足新的需求、使未来的维护更容易,以及能应对环境变化。如果没有精心规划和可执行的架构,就很难做到以上种种。

可维护性是一个长期的问题,应该从您的API的远景来看。考虑到这一点,你需要做出决定,实现未来的愿景而不是走那些看上去能过得更轻松的捷径。在一开始就做出艰难的决定将使你的项目有一个很长的生命周期,并提供用户所需的好处。

什么使软件架构具有高可维护性?如何评估API是否可被维护?

  • 我们的体系结构是否允许最小化对系统其他领域的影响甚至为零?

  • 对API的调试应该是容易的,不需要设置困难。我们应该建立模式并通过常用的方法(例如浏览器调试工具)。

  • 测试应该是自动化的,并且清晰明了,不能太复杂。

接口和实现

我的API体系结构的关键是使用C#接口来支持其他实现。如果您已经用C#编写了.NET代码,那么您可能已经使用了接口。我在解决方案中使用接口在领域层中构建一个契约,该契约保证我为API开发的任何数据层都遵循数据存储库的契约。它还允许我的API项目中的控制器遵守另一个已设立的契约,以获得正确的方法来处理领域项目的Supervisor中的API方法。接口对于.NET Core 是非常重要的,如果您需要了解更多的信息,请点击此处。

端口和适配器模式

我们希望整个API解决方案中的对象具有单一职责。这将使我们在需要修复缺陷或增强代码时让对象保持简单和易于修改。如果您的代码中有一些“代码异味”,那么您可能违反了单一责任原则。一般情况下,我关注接口契约的实现的长度和复杂性。我的方法中没有代码行限制,但是如果它已经超过了您IDE中的一个视图,那么它可能就太长了。此外,我还检查方法的圈复杂度,以确定项目方法和函数的复杂性。

端口和适配器模式(又称六角形架构)可以解决业务逻辑与其他依赖项(如数据访问或API框架)耦合过于紧密的问题。使用此模式将允许您的API解决方案具有清晰的边界、具有单一职责的良好命名的对象,最终使其更容易开发和维护。

我们可以很直观地把这个模式看作一个洋葱,端口位于六边形的外部,而适配器和业务逻辑的位置更靠近核心。我将架构的外部连接视为端口。被消费的API端点或Entity Framework Core 2.0所使用的数据库连接将成为典型的端口范例,而内部数据存储库则是适配器。

接下来,让我们看看架构的逻辑部分和一些演示代码示例。

640?wx_fmt=png

领域(Domain)层

在查看API和领域层之前,我们需要解释如何通过接口和API业务逻辑的实现构建契约。我们来看看领域层。领域层具有以下功能:

  • 定义将在整个解决方案中使用的实体对象。这些模型将表示数据层的数据模型(DataModel)。

  • 定义视图模型(ViewModel),将由API层针对HTTP请求和响应作为单个对象或对象集来使用。

  • 定义接口,我们的数据层可以通过这些接口实现数据访问逻辑。

  • 实现将包含从API层调用的方法的Supervisor。每个方法都代表一个API调用,并将数据从注入的数据层转换为视图模型以返回。

我们的域实体对象代表我们用来存储和检索用于API业务逻辑的数据的数据库。每个实体对象都将包含SQL表中的属性。如下即为照片实体Album。

public sealed class Album
{public int AlbumId { get; set; }public string Title { get; set; }public int ArtistId { get; set; }public ICollection<Track> Tracks { get; set; } = new HashSet<Track>();public Artist Artist { get; set; }
}

SQL数据库中的Album表有三表:AlbumId、Title和ArtistId。这三个属性是专辑实体的一部分,另外还有艺术家的名字以及相关艺术家和一组相关歌曲。正如我们将在API体系结构的其他层中看到的,我们将针对该项目中的视图模型构建此实体对象的定义。

视图模型是实体的扩展,并帮助为api的使用者提供更多的信息。让我们看看视图模型。它与相册实体非常相似,但具有额外的属性。在API的设计中,我确定每个相册应该在从API返回的有效负载中包含艺术家的名字。这能让API使用者拥有关于相册的关键信息,而不必在数据载荷中再传递Artist视图模型(特别是当我们返回大量Album时)。下面是我们的Album视图模型的一个示例。

public class AlbumViewModel
{public int AlbumId { get; set; }public string Title { get; set; }public int ArtistId { get; set; }public string ArtistName { get; set; }public ArtistViewModel Artist { get; set; }public IList<TrackViewModel> Tracks { get; set; }
}

在领域层中另一部分需要开发的是契约,它们会经过该层中为每个实体定义的接口。同样,我们将使用Album实体来展示所定义的接口。

public interface IAlbumRepository : IDisposable
{Task<List<Album>> GetAllAsync(CancellationToken ct = default(CancellationToken));
Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken));Task<List<Album>> GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken));Task<Album> AddAsync(Album newAlbum, CancellationToken ct = default(CancellationToken));Task<bool> UpdateAsync(Album album, CancellationToken ct = default(CancellationToken));Task<bool> DeleteAsync(int id, CancellationToken ct = default(CancellationToken));
}

如上例所示,接口定义了为Album实体实现数据访问方法所需的方法。每个实体对象和接口都有良好的定义和简单化,使下一层可以得到良好的定义。

最后,领域项目的核心是Supervisor类。它的用途是在实体和视图模型之间进行转换,并在API端点和数据访问逻辑之外执行业务逻辑。让Supervisor来处理这些还将隔离逻辑,使转换和业务逻辑能够进行单元测试。

查看获取和传递单个Album到API端点的Supervisor方法,我们可以看到将API前端连接到数据访问的逻辑注入到了Supervisor中,而仍然保持每个Album是独立的。

public async Task<AlbumViewModel> GetAlbumByIdAsync(int id, CancellationToken ct = default(CancellationToken))
{var albumViewModel = AlbumCoverter.Convert(await _albumRepository.GetByIdAsync(id, ct));albumViewModel.Artist = await GetArtistByIdAsync(albumViewModel.ArtistId, ct);albumViewModel.Tracks = await GetTrackByAlbumIdAsync(albumViewModel.AlbumId, ct);albumViewModel.ArtistName = albumViewModel.Artist.Name;return albumViewModel;
}

在领域项目中维护大部分代码和逻辑会使每个项目保持并遵守单一职责原则。

数据层

我们将看到的API体系结构的下一层是数据层。在我们所示例的解决方案中,使用的是Entity Framework Core 2.0。这意味着我们不仅拥有已定义的Entity Framework Core的DBContext,还有为SQL数据库中的每个实体生成的数据模型。如果我们以专辑实体的数据模型为例来看,会发现在数据库中存有三个属性,还有包含一组与专辑相关的歌曲,以及艺术家对象的相关属性。

虽然您可以拥有大量的数据层实现,但请记住,它必须遵守在领域层上记录的要求;每个数据层实现必须与领域层中详细的视图模型和存储库接口一起工作。我们为API开发的体系结构使用仓储模式将API层连接到数据层。使用依赖注入(正如我们前面讨论过的)对我们实现的每个存储库对象进行了处理。我们将讨论在着眼于API层时如何使用依赖项注入和代码。数据层的关键是使用领域层中开发的接口实现每个实体存储库。以领域层的专辑存储库为例,它就是实现了IAlbumRepository接口。每个存储库都将注入DBContext,允许使用实体框架核心访问SQL数据库。

public class AlbumRepository : IAlbumRepository
{private readonly ChinookContext _context;public AlbumRepository(ChinookContext context){_context = context;}private async Task<bool> AlbumExists(int id, CancellationToken ct = default(CancellationToken)){
return await GetByIdAsync(id, ct) != null;}public void Dispose(){_context.Dispose();}public async Task<List<Album>> GetAllAsync(CancellationToken ct = default(CancellationToken)){return await _context.Album.ToListAsync(ct);}public async Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken)){return await _context.Album.FindAsync(id);}public async Task<Album> AddAsync(Album newAlbum, CancellationToken ct = default(CancellationToken)){_context.Album.Add(newAlbum);await _context.SaveChangesAsync(ct);return newAlbum;}public async Task<bool> UpdateAsync(Album album, CancellationToken ct = default(CancellationToken)){if (!await AlbumExists(album.AlbumId, ct))return false;_context.Album.Update(album);_context.Update(album);await _context.SaveChangesAsync(ct);return true;}public async Task<bool> DeleteAsync(int id, CancellationToken ct = default(CancellationToken)){if (!await AlbumExists(id, ct))return false;var toRemove = _context.Album.Find(id);_context.Album.Remove(toRemove);await _context.SaveChangesAsync(ct);return true;}public async Task<List<Album>> GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken)){return await _context.Album.Where(a => a.ArtistId == id).ToListAsync(ct);}
}

拥有封装所有数据访问的数据层将有助于更好地测试API。我们可以构建多个数据访问实现:一个用于SQL数据库存储,另一个可以用于云NoSQL 存储模式,最后一个用于解决方案中的单元测试的模拟存储实现。

API层

我们将看到的最后一层是您的API使用者将发生交互的区域。这一层包含Web API端点逻辑的代码,包括控制器。这个解决方案的API项目将有一个单独的职责,那就是处理web服务器接收到的HTTP请求并返回HTTP响应,无论成功还是失败。在这个项目中,将会有非常少的业务逻辑。我们将处理在领域或数据项目中发生的异常和错误,以有效地与API的使用者进行通信。此通信将使用HTTP响应代码和在HTTP响应报文中返回的任何数据。

在ASP.NET Core 2.0 Web API,路由是使用Routing属性来处理的。如果您需要了解更多关于ASP.NET Core中Routing属性的内容,请移步此处。我们还使用依赖项注入将Supervisor分配给每个控制器。每个控制器的操作方法都有一个相应的Supervisor方法,用于处理API调用的逻辑。下面我有一个Album控制器的片段来展示这些概念。

[Route("api/[controller]")]
public class AlbumController : Controller
{private readonly IChinookSupervisor _chinookSupervisor;public AlbumController(IChinookSupervisor chinookSupervisor){_chinookSupervisor = chinookSupervisor;}[HttpGet][Produces(typeof(List<AlbumViewModel>))]public async Task<IActionResult> Get(CancellationToken ct = default(CancellationToken)){try{return new ObjectResult(await _chinookSupervisor.GetAllAlbumAsync(ct));}catch (Exception ex){return StatusCode(500, ex);}} ...
}

这个解决方案的Web API项目非常简略。我努力让这个解决方案中的代码尽可能的少,因为将来它可以被另一种交互形式所替代。

结论

正如我所展示的,设计和开发一个伟大的ASP.NET Core 2.0 Web API解决方案具有洞察力,以便拥有一个解耦的体系结构,该体系结构将允许每个层都是可测试的,并遵循单一的职责原则。我希望我的信息将允许您创建和维护您的产品Web api,以满足您的组织的需要。

关于作者

640?wx_fmt=jpegChris Woodruff (Woody) 拥有密歇根州立大学工程学院的计算机科学学位。Woody已经开发和架构软件解决方案超过20年,并且曾经致力于许多不同的平台和工具。他是一个社区领袖,为GRDevNight、GRDevDay、West Michigan Day of .NET和CodeMash之类的活动贡献过力量。他还帮助把广受欢迎的Give Camp活动带到西密歇根,那里的技术专业人士提供他们的时间和发展专业知识,以帮助当地的非营利组织。作为一个演讲者和播客作者,Woody已经讲过和讨论了很多话题,包括数据库设计和开源。他在Visual C#、数据平台和SQL方面一直是微软的MVP,并在2010年被公认为全球最优秀的20个MVPs之一。Woody是JetBrains的开发者,并且在北美推广.NET,.NET Core和JetBrains的产品。

.NET Core 最初是在2016年发布的,随着.NET Core 2.0的发布,微软拥有了下一个通用、模块化、跨平台和开源的平台主版本。.NETCore已经创建了许多API,在当前版本的.net框架中均可用。它最初是为下一代ASP.NET解决方案而创建的,但现在成了许多其他场景的驱动和基础,包括物联网、云计算和下一代移动解决方案。在本系列文章中,我们将探讨.NET Core的一些好处,以及它如何不仅能使传统的.NET开发人员受益,还能使所有需要为市场带来健壮、高效和经济的解决方案的技术人员受益。

原文地址:http://www.infoq.com/cn/articles/advanced-architecture-aspnet-core

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

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

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

相关文章

线段树-HDU5737-这题有点神

HDU5737 题意 [1][1][1]有长度为nnn的序列A,BA,BA,B [2]Q[2]Q[2]Q此操作两种类型 (1,l,r,x)(1,l,r,x)(1,l,r,x)将区间[l,r][l,r][l,r]的aia_iai​覆盖为xxx(2,l,r)(2,l,r)(2,l,r)询问区间[l,r][l,r][l,r]中有多少ai≥bia_i \ge b_iai​≥bi​ 题解 考虑用线段树维护. 重点考…

【高精】Oliver的成绩(jzoj 2008)

Oliver的成绩 题目大意&#xff1a; Oliver考了一次试&#xff0c;现在知道他的语数英的成绩&#xff0c;还有年级其他n个人的成绩&#xff0c;现在问Oliver三科各和年级第一差多少分&#xff0c;如果Oliver在这一科上是第一&#xff0c;则输出‘0’ 样例输入 10 10 10 …

P4070-[SDOI2016]生成魔咒【SA,平衡树】

正题 题目链接:https://www.luogu.com.cn/problem/P4070 题目大意 长度为nnn的字符串&#xff0c;对于每个iii求字符串1∼i1\sim i1∼i部分有多少个不同的子串。 解题思路 对于整个串ans∑i1nn−i1−heightians\sum_{i1}^nn-i1-height_ians∑i1n​n−i1−heighti​&#xff…

ASP.NET Core 2.1 使用Docker运行

1.新建一个 ASP.NET Core 2.1 项目然后运行一下项目&#xff0c;确保我们刚刚建立的项目可以正常运行。2.编写 Dockerfile新建一个文本文件&#xff0c;命名为 DockerfileFROM microsoft/dotnet:2.1-aspnetcore-runtimeWORKDIR /appCOPY . .EXPOSE 80ENTRYPOINT ["dotnet&…

【DP】和谐的奶牛(jzoj 1750)

和谐的奶牛 题目大意&#xff1a; 有一些括号&#xff08;保证是合法的&#xff0c;合法&#xff1a;每一个左括号都有自己配对的有括号&#xff09;&#xff0c;现在要将这些括号分为两组&#xff08;其中一组可以为空&#xff09;&#xff0c;分完组后括号的顺序要和原来的…

费用流-Wannafly Day2 TwoGraph-神题

TwoGraph 题意 题解 这真是一道神题,这题有两点比较难想,其中第一点是最难想的. 我们先考虑只有一张图的情况. 性质: [1]如果给每个点匹配一条边,形成一个(点,边)(点,边)(点,边)对,其中点不能重复出现,边也不能重复出现.那么这些对形成的图的联通块要么是树,要么是环套树. 证…

ASP.NET Core Web API 与 SSL

SSL一直没有真正研究过SSL&#xff0c;不知道下面的理解是否正确。SSL是Secure Sockets Layer的缩写&#xff0c;它用来保护服务器和客户端之前的通信。它是基于信任加密的概念。在介绍SSL的原理之前&#xff0c;首先介绍一下加密&#xff08;Encryption&#xff09;的概念。刷…

P5662-纪念品【dp】

正题 题目链接:https://www.luogu.com.cn/problem/P5662 题目大意 有TTT天&#xff0c;nnn个纪念品&#xff0c;每个纪念品每天的价格不同&#xff0c;数量不限。开始小明有mmm块钱&#xff0c;求最后一天的最多钱。 解题思路 因为一个纪念品可以一天卖出并且一天买入&#…

【模拟】聊天服务器的外部流量

聊天服务器的外部流量 题目大意&#xff1a; 有一个通讯系统&#xff0c;可以将某人邀请进来&#xff08;name&#xff09;&#xff0c;也可以将某人T出去&#xff08;-name&#xff09;&#xff0c;也可以向群中的所有人发一条信息&#xff08;name&#xff1a;…&#xff0…

线段树-Pudding Monster CF526F-单调栈

Pudding Monster 题目连接:https://www.luogu.org/problem/show?pidCF526F 问题提出 给长度为nnn的排列AAA.问有多少(l,r)(l,r)(l,r),使得将Al,Al1,...,ArA_l,A_{l1},...,A_rAl​,Al1​,...,Ar​排序之后是连续的一段数.n≤105n \le 10^5n≤105 问题解决 判断一段区间是否…

P5664-Emiya家今天的饭【dp】

正题 题目链接:https://www.luogu.com.cn/problem/P5664 题目大意 对于nnn个方法&#xff0c;mmm个材料&#xff0c;一个方法配对一个材料可以做an,ma_{n,m}an,m​道菜。选择kkk个配对要求 配对至少为k>1k>1k>1每个配对的方法不同每个材料最多用⌊k2⌋\lfloor\frac…

GitHub宣布GitHub Education 新计划,学校可免费用企业版

今天 GitHub 宣布面向所有学校和教育机构开放 GitHub Education&#xff0c;方便学生和老师免费使用 GitHub 企业版功能&#xff0c;以及学生开发者工具包、GitHub 教室、培训等资源。2014 年&#xff0c;GitHub 推出了学生开发者工具包&#xff0c;其中包括 Azure 等云服务、G…

【dfs】聚会

聚会 题目大意&#xff1a; 有一些树&#xff0c;求深的树的深度 原题 解题思路&#xff1a; 从一个根出发&#xff0c;dfs往下搜 代码&#xff1a; #include<cstdio> #include<iostream> #include<cstring> using namespace std; int n,t,g,w,ans,c[2…

P3960-列队【权值线段树】

正题 题目链接:https://www.luogu.com.cn/problem/P3960 题目大意 n∗mn*mn∗m的队列&#xff0c;起初站在第(i,j)(i,j)(i,j)位置的人编号是(i−1)∗nj(i-1)*nj(i−1)∗nj。然后每次选择一个人出队后所有人向左补齐后所有人向前补齐&#xff0c;然后刚刚出列的那个人入队。 求…

2018 大湾区(深圳) .NET技术分享交流会 第一期

.NET Core 2.1 已于2018年5月30日正式发布&#xff0c;邀请粤港澳大湾区.NET技术专家和从业人员&#xff0c;一起分享与交流.NET技术的发展方向&#xff0c;提高粤港澳大湾区.NET技术交流氛围&#xff0c;挖掘.NET高级人才&#xff0c;为改善.NET生态系统贡献力量&#xff0c;使…

线段树-Mex-洛谷P4137

Mex 问题提出 有一个长度为nnn的数组{a1,a2,…,an}\{a_1,a_2,…,a_n\}{a1​,a2​,…,an​}。mmm次询问&#xff0c;每次询问一个区间内最小没有出现过的自然数。 题目解答 对1−n1-n1−n这里能够的每个数xxx,都统计出来在数组中出现的位置,并在前补上000,在后补上n1n1n1. …

卡车

卡车 题目大意&#xff1a; 有一个卡车向里面放东西&#xff08;重量要么是一要么是二&#xff09;&#xff0c;价值不同&#xff0c;放的东西的价值之和最大是多少 原题&#xff1a; 解题思路&#xff1a; 看看数据就知道DP会MLE&#xff0c;所以要用排序来做&#xff0c;…

P2831-愤怒的小鸟【状压dp】

正题 题目链接:https://www.luogu.com.cn/problem/P2831 题目大意 nnn个点&#xff0c;每次可以射掉在函数yax2bxyax^2bxyax2bx上的点&#xff08;a,ba,ba,b自定但是要求a<0a<0a<0&#xff09;。求最少射击次数。 解题思路 考虑状压&#xff0c;我们发现如果一次射掉…

初一级模拟赛总结(3.15)

成绩&#xff1a; rankrankranknamenamenamescorescorescoreT1T1T1T2T2T2T3T3T3T4T4T4111lyflyflyf220220220100100100100100100000202020222fyfyfy150150150000100100100000505050333lthlthlth120120120100100100202020000000444hkyhkyhky100100100000100100100000000444tjhtj…

从明面上学习ASP.NET Core

一、前言这篇文章就是从能看到地方去学习Core&#xff0c;没有很深奥&#xff0c;也没有很难懂&#xff0c;现在我们开始吧。二、构建项目&#xff0c;引发思考创建项目的步骤真的很简单&#xff0c;你要是不会&#xff0c;我真也没法了&#xff0c;我这是创建的MVC的项目。接下…