NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

RAML 用户应遵循的 C#与 Web API 代码生成模式

  • 2016-06-15
  • 本文字数:4754 字

    阅读完需:约 16 分钟

在过去几年间,REST 规范的各种语言正在逐渐流行起来,例如 RAML Swagger 以及 API Blueprint 。但这些语言的主要范畴在于客户端工具,主要用于生成 JavaScript 或 TypeScript 文件、模拟对象(mock),以及对应的客户端单元测试。

与此同时,传统的.NET 后端开发者往往拥有 C#与 SQL 方面的经验,而对于如何暴露 REST 服务的各种细节缺乏兴趣。他们更乐于通过在 controller 的方法中添加一些路由特性(attribute)的方式完成任务,而在数据存储与服务端之间的通信方面发挥他们的核心竞争力。

这种方式通常会造成出现不计其数的信息传达错误,虽然这种错误并不太严重。一旦 UI 开发者与服务端开发者对于如何暴露某个 REST 终结点产生了分歧,就必须有人去更新他的代码。通常来说,这种更新只是一个较小的变更,但如果不断重复这一过程,则会造成开发者生产力的极大下降。

为了克服这一问题,UI 开发者可以寻求规规范语言的帮助,例如 RAML,以生成他们所需的 Web API 代码。而服务的开发者可专注于如何连接这些代码,而不是为路由特性和 HTTP 谓词生成各种模拟对象。

本文并不打算讨论如何使用 RAML,而是强调 RAML,或你所选择的规范语言需要为你生成怎样的代码。

C#代码生成的概念

C# 2.0 在设计时就考虑到了代码生成的问题。如今代码生成器的使用已经变得非常普遍,甚至包括 Visual Studio 本身。代码生成器可创建部分类(partial class)。一个部分类中包括组成整个类所需的部分代码,但未必是全部的代码。这就允许你将类的定义分散在多个文件中,其中部分代码是自动生成的,而另一部分则是手写的。这种分离性能够防止代码生成器删除开发者手写的代码。

不幸的是,这种方式还不完善。部分类允许你添加新的方法,但不能够修改现有方法的行为。因为这一点,我们不得不等待 2008 年所发布的 C# 3,其中引入了部分方法的概念。

从表面上看,部分方法与抽象方法非常相似,但这种比喻是错误的。抽象方法必须在某处实现,否则会使代码无法编译。而部分方法更类似于 C++ 中的空的宏,如果未实现某个部分方法,则编译器会直接取消对该方法的调用,就像这行调用代码从不存在一样。

部分方法的使用有一些严格的需求。由于编译器可能会取消该方法,因此不可返回任何类型,也不可以使用任何“out”参数。不过,你可以在部分方法中使用“ref”参数以返回某个值。由于在使用 ref 参数时必须在调用该部分方法之前为其赋值,那么即使该部分方法被删除,编译器仍然能够满足明确赋值的规则。

由于以上限制的存在,我们的实现将很大程度上依赖于 ref 参数。

Controller 的模式

所有的 REST 终结点都必须包含在某个 controller 类中,以下代码是一个简单的示例:

复制代码
[GeneratedCode("My Tool", "1.0.0.0")]
[RoutePrefix("api/customer")]
public partial class CustomerController : ApiController
{
//methods go here
}

以这种方式创建的 Web API controller 通常包含两种特性,通过中括号表示。GeneratedCode 表示这个类是由某个工具生成的,因此开发者不应直接修改它。RoutePrefix 这个可选的特性将用于基于特性的路由,我们稍后将展开讨论。

同步方法的模式

以下代码展示了一种可用于你的 Web API 代码生成的基本模式。

复制代码
[Route("search")]
[HttpGet]
public List<Customer> QueryCustomers(int zipCode, string lastName = "")
{
Tuple<List<Customer>> result = null;
QueryCustomers(zipCode, lastName, ref result);
if (result == null)
throw new NotImplementedException("RAML defined method wasn’t implemented");
else
return result.Item1;
}
partial void QueryCustomers(int zipCode, string lastName, ref Tuple<List<Customer>> result);

请注意一点,该部分方法将返回结果封装为一个元组(Tuple)对象,这样就使你能够区分“该方法未实现”以及“该方法返回 null”中 null 的不同意义。

我们之后将始终遵循这一模式以确保代码可编译。虽然对某个由 RAML 定义的 REST 方法进行变更可能会破坏构建,但如果仅仅是添加一个新方法,则不应当产生破坏性的后果。

每个 REST 方法应当至少包含两个特性。route 特性用于确定 URL 的形式,如果该类具有一个 RoutePrefix 特性,则方法中的 route 特性将附加在 RoutePrefix 特性之后。你可以通过 Mike Wasson 所撰写的文章“ Attribute Routing in ASP.NET Web API 2 ”学习这一主题的更多内容。

另一个特性则表现了该方法对应的谓词。从技术上说,谓词是可以从方法的名称中推断出来的,但一个明确的特性可使代码的阅读者更方便地快速理解其内容。而且由于这部分代码是自动生成的,因此不会造成额外的冗长感。

你可能还需要代码生成器为方法添加 Authorize 特性,表示该方法只能够由已登录的用户进行访问。

而无返回类型的 REST 方法看起来稍有一些不同之处:

复制代码
[Route("update")]
[HttpPost]
public void UpdateCustomer(Customer customer)
{
bool wasExecuted = false;
UpdateCustomer(customer, ref wasExecuted);
if (!wasExecuted)
throw new NotImplementedException("RAML defined method wasn’t implemented");
}
partial void UpdateCustomer(Customer customer, ref bool wasExecuted);

在这一模式中,方法的实现者需要将 wasExecuted 改为 true。

异步方法的模式

现如今,只要任何一个方法提供了异步的调用方式,那么同步的调用方式往往是受人鄙视的。虽然在延迟性方面表现得稍慢一些,但异步代码对于负载很大的服务器来说能够提供更好的吞吐性,从而提升整体的用户体验。

复制代码
[Route("search")]
[HttpGet]
public async Task<List<Customer>> QueryCustomersAsync(int
zipCode, string lastName = "")
{
Task<List<Customer>> resultTask = null;
QueryCustomersAsync(zipCode, lastName, ref resultTask);
if (resultTask == null)
throw new NotImplementedException("RAML defined method wasn’t implemented");
else
return await resultTask;
}
partial void QueryCustomersAsync(int zipCode, string lastName, ref
Task<List<Customer>> resultTask);

你需要注意的第一个不同之处在于返回类型被封装在一个 Task 中,这允许框架以异步方式等待该方法的完成。

为了获取 Task 对象其中的实际内容,你需要使用“await”关键字。即使该方法未返回任何值,该关键字也允许你等待该 Task 的完成。在异步上下文中绝对不要直接读取 Task.Result 中的内容,因为这可能会造成死锁。

而对于不返回值的 REST 方法,其模式也稍有不同。

复制代码
[Route("update")]
[HttpPost]
public async Task UpdateCustomerAsync(Customer customer)
{
Task resultTask = null;
UpdateCustomerAsync(customer, ref resultTask);
if (resultTask == null)
throw new NotImplementedException("RAML defined method wasn’t implemented");
else
await resultTask;
}
partial void UpdateCustomerAsync(Customer customer, ref Task resultTask);

支持客户端断开连接的情况

如果用户撤消了某个请求,或是断开了连接,那么取消一个运行时间较长的操作是很有益处的。为了在异步代码中实现这一点,你需要通过 ClientDisconnectedToken 侦听撤消操作。以下代码展示了一个使用该对象的示例:

复制代码
[Route("search")]
[HttpGet]
public async Task<List<Customer>> QueryCustomersCancellableAsync(int zipCode, string lastName = "",
CancellationToken cancellationToken = default(CancellationToken))
{
Task<List<Customer>> resultTask = null;
QueryCustomersCancellableAsync(zipCode, lastName, cancellationToken, ref resultTask);
if (resultTask == null)
throw new NotImplementedException("RAML defined method wasn’t implemented");
else
return await resultTask;
}
partial void QueryCustomersCancellableAsync(int zipCode, string
lastName, CancellationToken cancellationToken, ref
Task<List<Customer>> resultTask);

注意,cancellationtoken 在 REST 方法中被标记为可选的,即使框架会确保为其提供一个值,因此该参数可出现在任意可选参数之后的位置上。

Controller 基类

Web API controller 的标准基类是 ApiController,但服务的开发者可能会选择覆盖这个基类,为了能够进行覆盖,所生成的代码必须忽略这个基类。这就需要服务的开发者必须指定一个基类,否则该 API 就将变得不可见。

作为一个临时方案,代码生成器可以指定一个由服务开发者所命名的自定义基类,该基类需要继承自 ApiController,并包含任何共享的功能。

Model

当需要使用一些复杂对象时,代码生成器将试图生成这些类。虽然你可以使用一些纯粹的对象,但更好的方式是为其添加 DataContract 和 DataMember 这些特性的标注。这将允许服务开发者为其添加一些不需要暴露给客户端的额外属性(property),只要不将某个属性标注为 DataMember 即可。

复制代码
[DataContract]
public partial class Customer
{
[DataMember]
public int CustomerKey { get; set; }
[DataMember]
public string CustomerName { get; set; }
}

Model 的校验

为了对 model 进行校验,可以对需要检查的属性添加适当的特性。常见的校验特性包括 Required、MaxLength、MinLength、Phone 以及 EmailAddress。在 DataAnnotations 命名空间中包含了内置的校验特性的列表。

一旦定义了 model 校验逻辑之后,你还需要强制实施他们,可以在 REST 方法的开头添加这两行代码以实现该操作。

复制代码
if (!ModelState.IsValid)
throw new HttpResponseException(HttpStatusCode.BadRequest);

进阶应用

一旦你实现了基本的代码生成器之后,可以进一步探索可移除样板代码的场合。举例来说,你可以在生成的代码中加入对 username 的解析操作,将结果传递至部分方法中。比方说:

复制代码
[Route("search")]
[HttpGet]
public List<Customer> QueryCustomers(int zipCode, string lastName = "")
{
var user = [application specific logic]
Tuple<List<Customer>> result = null;
QueryCustomers(zipCode, lastName, ref result, user);
if (result == null)
throw new NotImplementedException();
else
return result.Item1;
}
partial void QueryCustomers(int zipCode, string lastName, ref
Tuple<List<Customer>> result, User user);

另一种移除样板代码的方式是使用一个数据上下文或其他资源,并传递给部分方法。你也可以选择通过日志记录的条目捕获请求与响应的信息。基本上,只要是公式化的或是重复性的操作,都可以按照这种方式进行简化。

付诸实践

RAML 与 C#的代码生成功能结合使用可极大地减少前端与后端开发团队之间的摩托。实现这种方式的秘密取决于一个可靠的设计步骤,即 JavaScript 与 C#的开发工作在开始具体编码之前需要对 RAML 达成一致。实现了这一点之后,当开发流程中出现各种不可避免的变更时,双方应当能够心平气和地接受对共享的 RAML 进行更改。

如果你希望发布与 RAML、代码生成、或是其他.NET 方面的文章,欢迎你通过 jonathan@infoq.com 联系 Jonathan Allen。如果你正在寻找某种将 RAML 转换至 C#的代码生成器,可以参考一下 Mulesoft Lab 发布的 RAML Tools for .NET

关于作者

Jonathan Allen的第一份工作是在上世纪 90 年代后期为某个诊所开发的 MIS 项目,该项目从早期的 Access 与 Excel 逐渐发展为一个企业级解决方案。之后,他又为财政部门编写自动交易系统达五年之久,在那之后,他决定转而进行高端用户界面的开发工作。在空余时间,他的兴趣是学习 15 世纪至 17 世纪的西方武术史并撰写相关文章。

查看英文原文 C#/Web API Code Generation Patterns for the RAML User

2016-06-15 19:233044
用户头像

发布了 428 篇内容, 共 172.1 次阅读, 收获喜欢 38 次。

关注

评论

发布
暂无评论
发现更多内容

MySQL 不完全入门指南

Java 编程 架构 面试 架构师

评DeepMind神经网络求解MIP论文:并非无所不能

杉数科技

神经网络 机器学习 算法 Deep learning 智能优化算法

Spring数据库事务典型错误用法剖析

博文视点Broadview

全凭阿里大牛总结的Java面试笔记,大专学历成功拿到35koffer

Java~~~

Java 架构 面试 JVM io

面试字节跳动java岗被算法吊打,60天苦修这些笔记,侥幸收获offer

Java~~~

Java 架构 面试 算法 红黑树

终于学完国内算法第一人10年经验总结的数据结构与算法详解文档

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

【Vue2.x 源码学习】第三十九篇 - 组件部分 - 创建组件虚拟节点

Brave

源码 vue2 8月日更

抖音快手AI智能获客询盘软件系统开发内容

抖音快手短视频营销获客系统软件开发简介

MySQL优化-批量插入与1亿条数据效率COUNT

一个大红包

8月日更

云原生,开发者的黄金时代

阿里巴巴中间件

云计算 阿里云 云原生 中间件

【我和达梦的故事】 有奖征文活动开始啦,万元奖品池+现金奖励等你拿!

墨天轮

数据库 征文大赛 国产数据库 达梦

阿里p9的Java面试心路历程笔记,轻松拿到了90w年薪的Offer

Java~~~

Java spring 架构 面试 JVM

制造企业的数字化转型案例分享

一只数据鲸鱼

数据可视化 工业4.0 智慧工业

云原生,开发者的黄金时代

阿里巴巴云原生

云计算 阿里云 云原生 中间件

linux 工具之pstack/gstack

webrtc developer

抖音快手获客软件系统开发价格

抖音快手搜客系统开发方案

谷歌架构师分享gRPC与云原生应用开发Go和Java为例文档

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Regan Yue带你一起学习微软AZ-900认证的有关知识「 第IV章」

Regan Yue

云计算 微软 8月日更 微软认证

抖音快手短视频询盘获客系统开发搭建

抖音快手短视频SEO获客系统开发搭建

李克强签署国务院令 公布《关键信息基础设施安全保护条例》

郑州埃文科技

终于拿到了深入Java虚拟机:JVMG1GC的算法与实现文档

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Windows Server 2019 安装提要 (及 VS 2019 Build Tool)

hedzr

DevOps windows server 2019 server core visual studio 2019 build tool

搜房记

escray

生活记录 8月日更

LeetCode题解:219. 存在重复元素 II,哈希表,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

全靠阿里内部(珠峰版)Java面试笔记,成功拿下12家大厂offer

Java~~~

Java MySQL 数据库 架构 面试

抖音快手询盘获客系统软件开发介绍

1-5年Java面试者必备:一线名企各专题面试笔记+java核心宝典pdf

Java~~~

Java MySQL 架构 面试 架构师

波场链DAPP开发|波场链TRONCHAIN介绍

量化系统19942438797

波场DAPP

RAML用户应遵循的C#与Web API代码生成模式_.NET_Jonathan Allen_InfoQ精选文章