.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中找到。
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
要了解更多关于.NET Core 依赖注入的信息,我建议您阅读以下关于MSDN的文档:ASP.NET中依赖注入的介绍。
现在我们来看看为什么要像我一样做API架构设计的原理。设计任何架构的两个方面都依赖于这两个概念:允许深度可维护性,以及在解决方案中使用经过验证的模式和架构。
对于任何工程过程来说,可维护性是指一个产品易于被维护:发现缺陷、纠正发现的缺陷、无需替换仍在工作的组件即可修复或更换有缺陷的组件、预防意外故障、最大限度地提高产品的使用寿命、有能力满足新的需求、使未来的维护更容易,以及能应对环境变化。如果没有精心规划和可执行的架构,就很难做到以上种种。
可维护性是一个长期的问题,应该从您的API的远景来看。考虑到这一点,你需要做出决定,实现未来的愿景而不是走那些看上去能过得更轻松的捷径。在一开始就做出艰难的决定将使你的项目有一个很长的生命周期,并提供用户所需的好处。
什么使软件架构具有高可维护性?如何评估API是否可被维护?
我们的体系结构是否允许最小化对系统其他领域的影响甚至为零?
对API的调试应该是容易的,不需要设置困难。我们应该建立模式并通过常用的方法(例如浏览器调试工具)。
测试应该是自动化的,并且清晰明了,不能太复杂。
我的API体系结构的关键是使用C#接口来支持其他实现。如果您已经用C#编写了.NET代码,那么您可能已经使用了接口。我在解决方案中使用接口在领域层中构建一个契约,该契约保证我为API开发的任何数据层都遵循数据存储库的契约。它还允许我的API项目中的控制器遵守另一个已设立的契约,以获得正确的方法来处理领域项目的Supervisor中的API方法。接口对于.NET Core 是非常重要的,如果您需要了解更多的信息,请点击此处。
我们希望整个API解决方案中的对象具有单一职责。这将使我们在需要修复缺陷或增强代码时让对象保持简单和易于修改。如果您的代码中有一些“代码异味”,那么您可能违反了单一责任原则。一般情况下,我关注接口契约的实现的长度和复杂性。我的方法中没有代码行限制,但是如果它已经超过了您IDE中的一个视图,那么它可能就太长了。此外,我还检查方法的圈复杂度,以确定项目方法和函数的复杂性。
端口和适配器模式(又称六角形架构)可以解决业务逻辑与其他依赖项(如数据访问或API框架)耦合过于紧密的问题。使用此模式将允许您的API解决方案具有清晰的边界、具有单一职责的良好命名的对象,最终使其更容易开发和维护。
我们可以很直观地把这个模式看作一个洋葱,端口位于六边形的外部,而适配器和业务逻辑的位置更靠近核心。我将架构的外部连接视为端口。被消费的API端点或Entity Framework Core 2.0所使用的数据库连接将成为典型的端口范例,而内部数据存储库则是适配器。
接下来,让我们看看架构的逻辑部分和一些演示代码示例。
在查看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> > GetAllAsync(CancellationToken ct = default(CancellationToken));
Task<Album> GetByIdAsync(int id, CancellationToken ct = default(CancellationToken));
Task<List> > 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> > 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> > GetByArtistIdAsync(int id, CancellationToken ct = default(CancellationToken))
{
return await _context.Album.Where(a => a.ArtistId == id).ToListAsync(ct);
}
}
拥有封装所有数据访问的数据层将有助于更好地测试API。我们可以构建多个数据访问实现:一个用于SQL数据库存储,另一个可以用于云NoSQL 存储模式,最后一个用于解决方案中的单元测试的模拟存储实现。
我们将看到的最后一层是您的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,以满足您的组织的需要。
Chris 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